diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index d7c93824..bf2526b8 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -93,12 +93,20 @@ private function getMethodReturnTypeForHydrationMode( Type $queryResultType ): Type { - if ($queryResultType instanceof VoidType) { + $isVoidType = (new VoidType())->isSuperTypeOf($queryResultType); + + if ($isVoidType->yes()) { // A void query result type indicates an UPDATE or DELETE query. // In this case all methods return the number of affected rows. return new IntegerType(); } + if ($isVoidType->maybe()) { + // We can't be sure what the query type is, so we return the + // declared return type of the method. + return $this->originalReturnType($methodReflection); + } + if (!$this->isObjectHydrationMode($hydrationMode)) { // We support only HYDRATE_OBJECT. For other hydration modes, we // return the declared return type of the method. diff --git a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php index 0cd65f58..a9b47e9a 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php +++ b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php @@ -44,4 +44,26 @@ public function testQueryResultTypeIsMixedWhenDQLIsInvalid(EntityManagerInterfac assertType('Doctrine\ORM\Query', $query); } + public function testQueryResultTypeIsVoidWithDeleteOrUpdate(EntityManagerInterface $em): void + { + $query = $em->getRepository(Many::class) + ->createQueryBuilder('m') + ->where('m.id IN (:ids)') + ->setParameter('ids', $ids) + ->delete() + ->getQuery(); + + assertType('Doctrine\ORM\Query', $query); + + $query = $em->getRepository(Many::class) + ->createQueryBuilder('m') + ->where('m.id IN (:ids)') + ->setParameter('ids', $ids) + ->update() + ->set('m.intColumn', '42') + ->getQuery(); + + assertType('Doctrine\ORM\Query', $query); + + } } diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index 064bd7ce..1b583bb1 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -4,6 +4,7 @@ use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Query; use function PHPStan\Testing\assertType; class QueryResultTest @@ -210,4 +211,110 @@ public function testReturnTypeOfQueryMethodsWithExplicitNonConstantHydrationMode $query->getOneOrNullResult($hydrationMode) ); } + + /** + * Test that we return the original return type when ResultType may be + * VoidType + * + * @param Query $query + */ + public function testReturnTypeOfQueryMethodsWithReturnTypeIsMixed(EntityManagerInterface $em, Query $query): void + { + assertType( + 'mixed', + $query->getResult(AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'mixed', + $query->execute(null, AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'mixed', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'mixed', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_OBJECT) + ); + } + + /** + * Test that we return the original return type when ResultType may be + * VoidType (TemplateType variant) + * + * @template T + * + * @param Query $query + */ + public function testReturnTypeOfQueryMethodsWithReturnTypeIsTemplateMixedType(EntityManagerInterface $em, Query $query): void + { + assertType( + 'mixed', + $query->getResult(AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'mixed', + $query->execute(null, AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'mixed', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'mixed', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_OBJECT) + ); + } + + + /** + * Test that we return ResultType return ResultType can not be VoidType + * + * @template T of array|object + * + * @param Query $query + */ + public function testReturnTypeOfQueryMethodsWithReturnTypeIsNonVoidTemplate(EntityManagerInterface $em, Query $query): void + { + assertType( + 'array', + $query->getResult(AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'array', + $query->execute(null, AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'array', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'array', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + 'T of array|object (method QueryResult\queryResult\QueryResultTest::testReturnTypeOfQueryMethodsWithReturnTypeIsNonVoidTemplate(), argument)', + $query->getSingleResult(AbstractQuery::HYDRATE_OBJECT) + ); + assertType( + '(T of array|object (method QueryResult\queryResult\QueryResultTest::testReturnTypeOfQueryMethodsWithReturnTypeIsNonVoidTemplate(), argument))|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_OBJECT) + ); + } }