From 63d35296f480088f2dd324b85dcc7fce556b1354 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 25 Oct 2025 18:12:52 +0200 Subject: [PATCH 1/6] Introduce default hydration mode --- .../CreateQueryDynamicReturnTypeExtension.php | 20 +++++++---- .../HydrationModeReturnTypeResolver.php | 7 ++-- .../QueryResultDynamicReturnTypeExtension.php | 7 +--- src/Type/Doctrine/Query/QueryType.php | 14 ++++++-- ...lderGetQueryDynamicReturnTypeExtension.php | 16 ++++++--- stubs/ORM/AbstractQuery.stub | 11 ++++++ stubs/ORM/Query.stub | 3 +- stubs/ORM/QueryBuilder.stub | 2 +- .../ORM/data/queryBuilder.php | 2 +- ...QueryResultTypeWalkerHydrationModeTest.php | 1 - .../Doctrine/data/QueryResult/createQuery.php | 12 +++---- .../queryBuilderExpressionTypeResolver.php | 12 +++---- .../data/QueryResult/queryBuilderGetQuery.php | 22 ++++++------ .../Doctrine/data/QueryResult/queryResult.php | 36 +++++++++---------- 14 files changed, 100 insertions(+), 65 deletions(-) diff --git a/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php index b78f8467..791d1e26 100644 --- a/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php @@ -16,6 +16,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\Doctrine\Query\QueryResultTypeBuilder; use PHPStan\Type\Doctrine\Query\QueryResultTypeWalker; use PHPStan\Type\Doctrine\Query\QueryType; @@ -76,7 +77,7 @@ public function getTypeFromMethodCall( if (!isset($args[$queryStringArgIndex])) { return new GenericObjectType( Query::class, - [new MixedType(), new MixedType()], + [new MixedType(), new MixedType(), new MixedType()], ); } @@ -95,21 +96,28 @@ public function getTypeFromMethodCall( } $typeBuilder = new QueryResultTypeBuilder(); + $query = $em->createQuery($queryString); + $hydrationMode = ConstantTypeHelper::getTypeFromValue($query->getHydrationMode()); try { - $query = $em->createQuery($queryString); QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry, $this->phpVersion, $this->driverDetector); } catch (ORMException | DBALException | NewDBALException | CommonException | MappingException | \Doctrine\ORM\Exception\ORMException $e) { - return new QueryType($queryString, null, null); + return new QueryType($queryString, null, null, null, $hydrationMode); } catch (AssertionError $e) { - return new QueryType($queryString, null, null); + return new QueryType($queryString, null, null, null, $hydrationMode); } - return new QueryType($queryString, $typeBuilder->getIndexType(), $typeBuilder->getResultType()); + return new QueryType( + $queryString, + $typeBuilder->getIndexType(), + $typeBuilder->getResultType(), + null, + $hydrationMode, + ); } return new GenericObjectType( Query::class, - [new MixedType(), new MixedType()], + [new MixedType(), new MixedType(), new MixedType()], ); }); } diff --git a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php index e978ae2f..751ee4de 100644 --- a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php +++ b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php @@ -3,7 +3,6 @@ namespace PHPStan\Type\Doctrine; use Doctrine\ORM\AbstractQuery; -use Doctrine\Persistence\ObjectManager; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -25,7 +24,7 @@ public function getMethodReturnTypeForHydrationMode( Type $hydrationMode, Type $queryKeyType, Type $queryResultType, - ?ObjectManager $objectManager + ?Type $defaultHydrationModeType = null ): ?Type { $isVoidType = (new VoidType())->isSuperTypeOf($queryResultType); @@ -42,6 +41,10 @@ public function getMethodReturnTypeForHydrationMode( return null; } + if ($defaultHydrationModeType !== null && $hydrationMode->isNull()->yes()) { + $hydrationMode = $defaultHydrationModeType; + } + if (!$hydrationMode instanceof ConstantIntegerType) { return null; } diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 22ed0b5a..6cda1598 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -9,7 +9,6 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Doctrine\HydrationModeReturnTypeResolver; -use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; @@ -27,16 +26,12 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn 'getSingleResult' => 0, ]; - private ObjectMetadataResolver $objectMetadataResolver; - private HydrationModeReturnTypeResolver $hydrationModeReturnTypeResolver; public function __construct( - ObjectMetadataResolver $objectMetadataResolver, HydrationModeReturnTypeResolver $hydrationModeReturnTypeResolver ) { - $this->objectMetadataResolver = $objectMetadataResolver; $this->hydrationModeReturnTypeResolver = $hydrationModeReturnTypeResolver; } @@ -84,7 +79,7 @@ public function getTypeFromMethodCall( $hydrationMode, $queryType->getTemplateType(AbstractQuery::class, 'TKey'), $queryType->getTemplateType(AbstractQuery::class, 'TResult'), - $this->objectMetadataResolver->getObjectManager(), + $queryType->getTemplateType(AbstractQuery::class, 'THydrationMode'), ); } diff --git a/src/Type/Doctrine/Query/QueryType.php b/src/Type/Doctrine/Query/QueryType.php index 53842dd2..2eb8b999 100644 --- a/src/Type/Doctrine/Query/QueryType.php +++ b/src/Type/Doctrine/Query/QueryType.php @@ -15,16 +15,26 @@ class QueryType extends GenericObjectType private Type $resultType; + private Type $defaultHydrationModeType; + private string $dql; - public function __construct(string $dql, ?Type $indexType = null, ?Type $resultType = null, ?Type $subtractedType = null) + public function __construct( + string $dql, + ?Type $indexType = null, + ?Type $resultType = null, + ?Type $subtractedType = null, + ?Type $defaultHydrationModeType = null + ) { $this->indexType = $indexType ?? new MixedType(); $this->resultType = $resultType ?? new MixedType(); + $this->defaultHydrationModeType = $defaultHydrationModeType ?? new MixedType(); parent::__construct('Doctrine\ORM\Query', [ $this->indexType, $this->resultType, + $this->defaultHydrationModeType, ], $subtractedType); $this->dql = $dql; @@ -41,7 +51,7 @@ public function equals(Type $type): bool public function changeSubtractedType(?Type $subtractedType): Type { - return new self('Doctrine\ORM\Query', $this->indexType, $this->resultType, $subtractedType); + return new self('Doctrine\ORM\Query', $this->indexType, $this->resultType, $subtractedType, $this->defaultHydrationModeType); } public function isSuperTypeOf(Type $type): IsSuperTypeOfResult diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php index 98fdefb3..a4013635 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php @@ -15,6 +15,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Doctrine\ORM\DynamicQueryBuilderArgumentException; +use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\Doctrine\ArgumentsProcessor; use PHPStan\Type\Doctrine\DescriptorRegistry; use PHPStan\Type\Doctrine\DoctrineTypeUtils; @@ -197,17 +198,24 @@ private function getQueryType(string $dql): Type } $typeBuilder = new QueryResultTypeBuilder(); + $query = $em->createQuery($dql); + $hydrationMode = ConstantTypeHelper::getTypeFromValue($query->getHydrationMode()); try { - $query = $em->createQuery($dql); QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry, $this->phpVersion, $this->driverDetector); } catch (ORMException | DBALException | CommonException | MappingException | \Doctrine\ORM\Exception\ORMException $e) { - return new QueryType($dql, null); + return new QueryType($dql, null, null, null, $hydrationMode); } catch (AssertionError $e) { - return new QueryType($dql, null); + return new QueryType($dql, null, null, null, $hydrationMode); } - return new QueryType($dql, $typeBuilder->getIndexType(), $typeBuilder->getResultType()); + return new QueryType( + $dql, + $typeBuilder->getIndexType(), + $typeBuilder->getResultType(), + null, + $hydrationMode, + ); } } diff --git a/stubs/ORM/AbstractQuery.stub b/stubs/ORM/AbstractQuery.stub index 25f7e320..7000a3e0 100644 --- a/stubs/ORM/AbstractQuery.stub +++ b/stubs/ORM/AbstractQuery.stub @@ -9,6 +9,7 @@ use Doctrine\ORM\NoResultException; /** * @template-covariant TKey The type of column used in indexBy * @template-covariant TResult The type of results returned by this query in HYDRATE_OBJECT mode + * @template-covariant THydrationMode = AbstractQuery::HYDRATE_OBJECT The default hydration mode when none is provided */ abstract class AbstractQuery { @@ -84,4 +85,14 @@ abstract class AbstractQuery { } + /** + * @template TNewHydrationMode of string|AbstractQuery::HYDRATE_* + * @param TNewHydrationMode $hydrationMode + * @phpstan-self-out self + * @return self + */ + public function setHydrationMode($hydrationMode): static + { + } + } diff --git a/stubs/ORM/Query.stub b/stubs/ORM/Query.stub index 77c2db2e..d8208f1a 100644 --- a/stubs/ORM/Query.stub +++ b/stubs/ORM/Query.stub @@ -5,8 +5,9 @@ namespace Doctrine\ORM; /** * @template-covariant TKey The type of column used in indexBy * @template-covariant TResult The type of results returned by this query in HYDRATE_OBJECT mode + * @template-covariant THydrationMode = AbstractQuery::HYDRATE_OBJECT The default hydration mode when none is provided * - * @extends AbstractQuery + * @extends AbstractQuery */ final class Query extends AbstractQuery { diff --git a/stubs/ORM/QueryBuilder.stub b/stubs/ORM/QueryBuilder.stub index 46b8e0ee..7b8cdd3f 100644 --- a/stubs/ORM/QueryBuilder.stub +++ b/stubs/ORM/QueryBuilder.stub @@ -17,7 +17,7 @@ class QueryBuilder } /** - * @return Query + * @return Query */ public function getQuery() { diff --git a/tests/DoctrineIntegration/ORM/data/queryBuilder.php b/tests/DoctrineIntegration/ORM/data/queryBuilder.php index c0c10138..6a701926 100644 --- a/tests/DoctrineIntegration/ORM/data/queryBuilder.php +++ b/tests/DoctrineIntegration/ORM/data/queryBuilder.php @@ -67,7 +67,7 @@ public function usingMethodThatReturnStatic(): ?MyEntity ]); $result = $queryBuilder->getQuery()->getOneOrNullResult(); - assertType('mixed', $result); + assertType('PHPStan\DoctrineIntegration\ORM\CustomRepositoryUsage\MyEntity|null', $result); return $result; } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index 4c86b2b3..26137efa 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -91,7 +91,6 @@ public function test(Type $expectedType, string $dql, string $methodName, ?int $ new ConstantIntegerType($this->getRealHydrationMode($methodName, $hydrationMode)), $typeBuilder->getIndexType(), $typeBuilder->getResultType(), - $entityManager, ) ?? new MixedType(); self::assertSame( diff --git a/tests/Type/Doctrine/data/QueryResult/createQuery.php b/tests/Type/Doctrine/data/QueryResult/createQuery.php index e15515b7..d3e9edb3 100644 --- a/tests/Type/Doctrine/data/QueryResult/createQuery.php +++ b/tests/Type/Doctrine/data/QueryResult/createQuery.php @@ -15,42 +15,42 @@ public function testQueryTypeParametersAreInfered(EntityManagerInterface $em): v FROM QueryResult\Entities\Many m '); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); $query = $em->createQuery(' SELECT m.intColumn, m.stringNullColumn FROM QueryResult\Entities\Many m '); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } public function testQueryTypeSimpleArray(EntityManagerInterface $em): void { $query = $em->createQuery('SELECT m.simpleArrayColumn FROM QueryResult\Entities\Many m'); - assertType('Doctrine\ORM\Query}>', $query); + assertType('Doctrine\ORM\Query}, 1>', $query); } public function testMappingError(EntityManagerInterface $em): void { $query = $em->createQuery('SELECT u.foo FROM ' . CreateQuery::class . ' u'); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } public function testQueryResultTypeIsMixedWhenDQLIsNotKnown(EntityManagerInterface $em, string $dql): void { $query = $em->createQuery($dql); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } public function testQueryResultTypeIsMixedWhenDQLIsInvalid(EntityManagerInterface $em, string $dql): void { $query = $em->createQuery('invalid'); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } } diff --git a/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php b/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php index 2a6af1d9..67e6f677 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php +++ b/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php @@ -25,15 +25,15 @@ public function testQueryTypeIsInferredOnAcrossMethods(EntityManagerInterface $e $query = $this->getQueryBuilder($em)->getQuery(); $branchingQuery = $this->getBranchingQueryBuilder($em)->getQuery(); - assertType('Doctrine\ORM\Query', $query); - assertType('Doctrine\ORM\Query#1|Doctrine\ORM\Query#2', $branchingQuery); + assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query#1|Doctrine\ORM\Query#2', $branchingQuery); } public function testQueryTypeIsInferredOnAcrossMethodsEvenWhenVariableAssignmentIsUsed(EntityManagerInterface $em): void { $queryBuilder = $this->getQueryBuilder($em); - assertType('Doctrine\ORM\Query', $queryBuilder->getQuery()); + assertType('Doctrine\ORM\Query', $queryBuilder->getQuery()); } public function testQueryBuilderPassedElsewhereNotTracked(EntityManagerInterface $em): void @@ -43,14 +43,14 @@ public function testQueryBuilderPassedElsewhereNotTracked(EntityManagerInterface $this->adjustQueryBuilderToIndexByInt($queryBuilder); - assertType('Doctrine\ORM\Query', $queryBuilder->getQuery()); + assertType('Doctrine\ORM\Query', $queryBuilder->getQuery()); } public function testDiveIntoCustomEntityRepository(EntityManagerInterface $em): void { $queryBuilder = $this->myRepository->getCustomQueryBuilder($em); - assertType('Doctrine\ORM\Query', $queryBuilder->getQuery()); + assertType('Doctrine\ORM\Query', $queryBuilder->getQuery()); } @@ -58,7 +58,7 @@ public function testStaticCallWorksToo(EntityManagerInterface $em): void { $queryBuilder = self::getStaticQueryBuilder($em); - assertType('Doctrine\ORM\Query', $queryBuilder->getQuery()); + assertType('Doctrine\ORM\Query', $queryBuilder->getQuery()); } public function testFirstClassCallableDoesNotFail(EntityManagerInterface $em): void diff --git a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php index c0cf3068..0e9d9e91 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php +++ b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php @@ -40,14 +40,14 @@ public function testQueryTypeParametersAreInfered(EntityManagerInterface $em): v ->from(Many::class, 'm') ->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); $query = $em->createQueryBuilder() ->select(['m.intColumn', 'm.stringNullColumn']) ->from(Many::class, 'm') ->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } public function testIndexByInfering(EntityManagerInterface $em): void @@ -57,14 +57,14 @@ public function testIndexByInfering(EntityManagerInterface $em): void ->from(Many::class, 'm', 'm.intColumn') ->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); $query = $em->createQueryBuilder() ->select('m') ->from(Many::class, 'm', 'm.stringColumn') ->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); $query = $em->createQueryBuilder() ->select(['m.intColumn', 'm.stringNullColumn']) @@ -72,7 +72,7 @@ public function testIndexByInfering(EntityManagerInterface $em): void ->indexBy('m', 'm.stringColumn') ->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } public function testIndexByResultInfering(EntityManagerInterface $em): void @@ -113,14 +113,14 @@ public function testConditionalAddSelect(EntityManagerInterface $em, bool $bool) } $query = $qb->from(Many::class, 'm')->getQuery(); - assertType('Doctrine\ORM\Query|Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query|Doctrine\ORM\Query', $query); } public function testQueryResultTypeIsMixedWhenDQLIsNotKnown(QueryBuilder $builder): void { $query = $builder->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } public function testQueryResultTypeIsMixedWhenDQLIsInvalid(EntityManagerInterface $em): void @@ -130,7 +130,7 @@ public function testQueryResultTypeIsMixedWhenDQLIsInvalid(EntityManagerInterfac ->from(Many::class, 'm') ->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } public function testQueryResultTypeIsMixedWhenDQLIsUsingAnInterfaceTypeDefinition(EntityManagerInterface $em): void @@ -144,7 +144,7 @@ public function testQueryResultTypeIsMixedWhenDQLIsUsingAnInterfaceTypeDefinitio ->from(get_class($vehicle), 'v') ->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } public function testQueryResultTypeIsVoidWithDeleteOrUpdate(EntityManagerInterface $em): void @@ -156,7 +156,7 @@ public function testQueryResultTypeIsVoidWithDeleteOrUpdate(EntityManagerInterfa ->delete() ->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); $query = $em->getRepository(Many::class) ->createQueryBuilder('m') @@ -166,7 +166,7 @@ public function testQueryResultTypeIsVoidWithDeleteOrUpdate(EntityManagerInterfa ->set('m.intColumn', '42') ->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index 54eef205..72dc7615 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -17,14 +17,14 @@ public function testQueryTypeParametersAreInfered(EntityManagerInterface $em): v FROM QueryResult\Entities\Many m '); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); $query = $em->createQuery(' SELECT m.intColumn, m.stringNullColumn FROM QueryResult\Entities\Many m '); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } @@ -50,23 +50,23 @@ public function testReturnTypeOfQueryMethodsWithImplicitHydrationMode(EntityMana $query->toIterable() ); assertType( - 'mixed', + 'list', $query->execute() ); assertType( - 'mixed', + 'list', $query->executeIgnoreQueryCache() ); assertType( - 'mixed', + 'list', $query->executeUsingQueryCache() ); assertType( - 'mixed', + 'QueryResult\Entities\Many', $query->getSingleResult() ); assertType( - 'mixed', + 'QueryResult\Entities\Many|null', $query->getOneOrNullResult() ); } @@ -429,17 +429,17 @@ public function testQueryMethods(EntityManagerInterface $em): void { $q = 'SELECT m FROM QueryResult\Entities\Many m'; - assertType('Doctrine\ORM\Query', $em->createQuery($q)->setLockMode(LockMode::NONE)); - assertType('Doctrine\ORM\Query', $em->createQuery($q)->setParameter(1, 1)); - assertType('Doctrine\ORM\Query', $em->createQuery($q)->setMaxResults(10)); - assertType('Doctrine\ORM\Query', $em->createQuery($q)->setCacheable(true)); - assertType('Doctrine\ORM\Query', $em->createQuery($q)->setLifetime(1)); - assertType('Doctrine\ORM\Query', $em->createQuery($q)->disableResultCache()); - assertType('Doctrine\ORM\Query', $em->createQuery($q)->enableResultCache(1)); - assertType('Doctrine\ORM\Query', $em->createQuery($q)->setResultCacheLifetime(1)); - assertType('Doctrine\ORM\Query', $em->createQuery($q)->setResultCacheProfile(null)); - assertType('Doctrine\ORM\Query', $em->createQuery($q)->setHint('name', 1)); - assertType('Doctrine\ORM\Query', $em->createQuery($q)->setHydrationMode(AbstractQuery::HYDRATE_OBJECT)); + assertType('Doctrine\ORM\Query', $em->createQuery($q)->setLockMode(LockMode::NONE)); + assertType('Doctrine\ORM\Query', $em->createQuery($q)->setParameter(1, 1)); + assertType('Doctrine\ORM\Query', $em->createQuery($q)->setMaxResults(10)); + assertType('Doctrine\ORM\Query', $em->createQuery($q)->setCacheable(true)); + assertType('Doctrine\ORM\Query', $em->createQuery($q)->setLifetime(1)); + assertType('Doctrine\ORM\Query', $em->createQuery($q)->disableResultCache()); + assertType('Doctrine\ORM\Query', $em->createQuery($q)->enableResultCache(1)); + assertType('Doctrine\ORM\Query', $em->createQuery($q)->setResultCacheLifetime(1)); + assertType('Doctrine\ORM\Query', $em->createQuery($q)->setResultCacheProfile(null)); + assertType('Doctrine\ORM\Query', $em->createQuery($q)->setHint('name', 1)); + assertType('Doctrine\ORM\Query', $em->createQuery($q)->setHydrationMode(AbstractQuery::HYDRATE_ARRAY)); } } From 3b6cdd2a9fb9d34f053196fd628ba293bdb3410b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 25 Oct 2025 19:35:51 +0200 Subject: [PATCH 2/6] Try --- stubs/ORM/AbstractQuery.stub | 4 ++-- stubs/ORM/Query.stub | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stubs/ORM/AbstractQuery.stub b/stubs/ORM/AbstractQuery.stub index 7000a3e0..bcc5519f 100644 --- a/stubs/ORM/AbstractQuery.stub +++ b/stubs/ORM/AbstractQuery.stub @@ -9,7 +9,7 @@ use Doctrine\ORM\NoResultException; /** * @template-covariant TKey The type of column used in indexBy * @template-covariant TResult The type of results returned by this query in HYDRATE_OBJECT mode - * @template-covariant THydrationMode = AbstractQuery::HYDRATE_OBJECT The default hydration mode when none is provided + * @template-covariant THydrationMode The default hydration mode when none is provided */ abstract class AbstractQuery { @@ -89,7 +89,7 @@ abstract class AbstractQuery * @template TNewHydrationMode of string|AbstractQuery::HYDRATE_* * @param TNewHydrationMode $hydrationMode * @phpstan-self-out self - * @return self + * @return static */ public function setHydrationMode($hydrationMode): static { diff --git a/stubs/ORM/Query.stub b/stubs/ORM/Query.stub index d8208f1a..5d66fad4 100644 --- a/stubs/ORM/Query.stub +++ b/stubs/ORM/Query.stub @@ -5,7 +5,7 @@ namespace Doctrine\ORM; /** * @template-covariant TKey The type of column used in indexBy * @template-covariant TResult The type of results returned by this query in HYDRATE_OBJECT mode - * @template-covariant THydrationMode = AbstractQuery::HYDRATE_OBJECT The default hydration mode when none is provided + * @template-covariant THydrationMode The default hydration mode when none is provided * * @extends AbstractQuery */ From c722b84879a8ce1c1bce146c978c24e4c2701075 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 25 Oct 2025 23:27:50 +0200 Subject: [PATCH 3/6] Try default --- stubs/ORM/AbstractQuery.stub | 2 +- stubs/ORM/Query.stub | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stubs/ORM/AbstractQuery.stub b/stubs/ORM/AbstractQuery.stub index bcc5519f..4ff7f034 100644 --- a/stubs/ORM/AbstractQuery.stub +++ b/stubs/ORM/AbstractQuery.stub @@ -9,7 +9,7 @@ use Doctrine\ORM\NoResultException; /** * @template-covariant TKey The type of column used in indexBy * @template-covariant TResult The type of results returned by this query in HYDRATE_OBJECT mode - * @template-covariant THydrationMode The default hydration mode when none is provided + * @template-covariant THydrationMode of string|AbstractQuery::HYDRATE_* = string|AbstractQuery::HYDRATE_* The default hydration mode when none is provided */ abstract class AbstractQuery { diff --git a/stubs/ORM/Query.stub b/stubs/ORM/Query.stub index 5d66fad4..358f9f01 100644 --- a/stubs/ORM/Query.stub +++ b/stubs/ORM/Query.stub @@ -5,7 +5,7 @@ namespace Doctrine\ORM; /** * @template-covariant TKey The type of column used in indexBy * @template-covariant TResult The type of results returned by this query in HYDRATE_OBJECT mode - * @template-covariant THydrationMode The default hydration mode when none is provided + * @template-covariant THydrationMode of string|AbstractQuery::HYDRATE_* = string|AbstractQuery::HYDRATE_* The default hydration mode when none is provided * * @extends AbstractQuery */ From 62ddc87a2a83e4220143df2a49b260dfc321cfda Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 25 Oct 2025 23:33:46 +0200 Subject: [PATCH 4/6] Try static --- stubs/ORM/AbstractQuery.stub | 2 +- stubs/ORM/Query.stub | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/stubs/ORM/AbstractQuery.stub b/stubs/ORM/AbstractQuery.stub index 4ff7f034..5f3f5d0f 100644 --- a/stubs/ORM/AbstractQuery.stub +++ b/stubs/ORM/AbstractQuery.stub @@ -88,7 +88,7 @@ abstract class AbstractQuery /** * @template TNewHydrationMode of string|AbstractQuery::HYDRATE_* * @param TNewHydrationMode $hydrationMode - * @phpstan-self-out self + * @phpstan-self-out static * @return static */ public function setHydrationMode($hydrationMode): static diff --git a/stubs/ORM/Query.stub b/stubs/ORM/Query.stub index 358f9f01..421cc7db 100644 --- a/stubs/ORM/Query.stub +++ b/stubs/ORM/Query.stub @@ -11,4 +11,15 @@ namespace Doctrine\ORM; */ final class Query extends AbstractQuery { + + /** + * @template TNewHydrationMode of string|AbstractQuery::HYDRATE_* + * @param TNewHydrationMode $hydrationMode + * @phpstan-self-out static + * @return static + */ + public function setHydrationMode($hydrationMode): static + { + } + } From b2b9b1301cff6a7ad5b6d765e64f64522ea3f159 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 25 Oct 2025 23:52:47 +0200 Subject: [PATCH 5/6] Qb::getQuery --- .../QueryBuilderGetQueryDynamicReturnTypeExtension.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php index a4013635..6139c2bd 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php @@ -7,6 +7,7 @@ use Doctrine\DBAL\DBALException; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\ORMException; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\Mapping\MappingException; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Identifier; @@ -184,22 +185,25 @@ public function getTypeFromMethodCall( } } - $resultTypes[] = $this->getQueryType($queryBuilder->getDQL()); + $resultTypes[] = $this->getQueryType($queryBuilder); } return TypeCombinator::union(...$resultTypes); } - private function getQueryType(string $dql): Type + private function getQueryType(QueryBuilder $queryBuilder): Type { + $dql = $queryBuilder->getDQL(); + $em = $this->objectMetadataResolver->getObjectManager(); if (!$em instanceof EntityManagerInterface) { return new QueryType($dql, null); } + $hydrationMode = ConstantTypeHelper::getTypeFromValue($queryBuilder->getQuery()->getHydrationMode()); + $typeBuilder = new QueryResultTypeBuilder(); $query = $em->createQuery($dql); - $hydrationMode = ConstantTypeHelper::getTypeFromValue($query->getHydrationMode()); try { QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry, $this->phpVersion, $this->driverDetector); From ce8ea0577a89ed9d55b5194ba8a505a2888a568f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 25 Oct 2025 23:58:12 +0200 Subject: [PATCH 6/6] Use constant --- stubs/ORM/QueryBuilder.stub | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stubs/ORM/QueryBuilder.stub b/stubs/ORM/QueryBuilder.stub index 7b8cdd3f..464aad60 100644 --- a/stubs/ORM/QueryBuilder.stub +++ b/stubs/ORM/QueryBuilder.stub @@ -2,6 +2,7 @@ namespace Doctrine\ORM; +use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\Query\Expr; class QueryBuilder @@ -17,7 +18,7 @@ class QueryBuilder } /** - * @return Query + * @return Query */ public function getQuery() {