diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 75e8f8aae5a..81c4b67bc5a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -201,7 +201,7 @@ parameters: path: src/Proxy/ProxyFactory.php - - message: "#^Parameter \\#2 \\$sqlParams of method Doctrine\\\\ORM\\\\Query\\:\\:evictResultSetCache\\(\\) expects array\\, array\\ given\\.$#" + message: "#^Parameter \\#2 \\$sqlParams of method Doctrine\\\\ORM\\\\Query\\:\\:evictResultSetCache\\(\\) expects array\\, array\\ given\\.$#" count: 1 path: src/Query.php @@ -271,7 +271,7 @@ parameters: path: src/Query/SqlWalker.php - - message: "#^Parameter \\#2 \\$dqlPart of method Doctrine\\\\ORM\\\\QueryBuilder\\:\\:add\\(\\) expects array\\<'join'\\|int, array\\\\|string\\>\\|object\\|string, non\\-empty\\-array\\ given\\.$#" + message: "#^Parameter \\#2 \\$dqlPart of method Doctrine\\\\ORM\\\\QueryBuilder\\:\\:add\\(\\) expects array\\<'join'\\|int, array\\\\|string\\>\\|object\\|string, non\\-empty\\-array\\ given\\.$#" count: 2 path: src/QueryBuilder.php diff --git a/src/AbstractQuery.php b/src/AbstractQuery.php index 0ff92c30089..704562032be 100644 --- a/src/AbstractQuery.php +++ b/src/AbstractQuery.php @@ -44,6 +44,8 @@ * Base contract for ORM queries. Base class for Query and NativeQuery. * * @link www.doctrine-project.org + * + * @template T */ abstract class AbstractQuery { @@ -683,6 +685,12 @@ public function getHydrationMode(): string|int * Alias for execute(null, $hydrationMode = HYDRATE_OBJECT). * * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode + * + * @psalm-return ( + * $hydrationMode is self::HYDRATE_OBJECT|null + * ? array + * : mixed + * ) */ public function getResult(string|int $hydrationMode = self::HYDRATE_OBJECT): mixed { @@ -730,6 +738,12 @@ public function getScalarResult(): array * * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode * + * @psalm-return ( + * $hydrationMode is self::HYDRATE_OBJECT|null + * ? T|null + * : mixed + * ) + * * @throws NonUniqueResultException */ public function getOneOrNullResult(string|int|null $hydrationMode = null): mixed @@ -765,6 +779,12 @@ public function getOneOrNullResult(string|int|null $hydrationMode = null): mixed * * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode * + * @psalm-return ( + * $hydrationMode is self::HYDRATE_OBJECT|null + * ? T + * : mixed + * ) + * * @throws NonUniqueResultException If the query result is not unique. * @throws NoResultException If the query returned no result. */ diff --git a/src/EntityRepository.php b/src/EntityRepository.php index a53c5284881..9a963a21e43 100644 --- a/src/EntityRepository.php +++ b/src/EntityRepository.php @@ -49,6 +49,8 @@ public function __construct( /** * Creates a new QueryBuilder instance that is prepopulated for this entity name. + * + * @return QueryBuilder */ public function createQueryBuilder(string $alias, string|null $indexBy = null): QueryBuilder { diff --git a/src/NativeQuery.php b/src/NativeQuery.php index 6cee0e843fb..dd1a9646535 100644 --- a/src/NativeQuery.php +++ b/src/NativeQuery.php @@ -16,6 +16,7 @@ * Represents a native SQL query. * * @final + * @extends AbstractQuery */ class NativeQuery extends AbstractQuery { diff --git a/src/Query.php b/src/Query.php index 5b0ceb7a97c..6aa764cbb1b 100644 --- a/src/Query.php +++ b/src/Query.php @@ -37,6 +37,8 @@ /** * A Query object represents a DQL query. * + * @template T + * @extends AbstractQuery * @final */ class Query extends AbstractQuery diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index a6a39a964b8..6d2c8616ef4 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -38,6 +38,8 @@ /** * This class is responsible for building DQL query strings via an object oriented * PHP interface. + * + * @template T */ class QueryBuilder implements Stringable { @@ -251,6 +253,8 @@ public function getDQL(): string * $q = $qb->getQuery(); * $results = $q->execute(); * + * + * @psalm-return Query */ public function getQuery(): Query { @@ -613,6 +617,11 @@ public function add(string $dqlPartName, string|object|array $dqlPart, bool $app * * * @return $this + * @psalm-return static + * @phpstan-return $this + * + * @psalm-this-out static + * @phpstan-this-out $this */ public function select(mixed ...$select): static { diff --git a/tests/StaticAnalysis/query-builder.php b/tests/StaticAnalysis/query-builder.php new file mode 100644 index 00000000000..6487a41f83c --- /dev/null +++ b/tests/StaticAnalysis/query-builder.php @@ -0,0 +1,56 @@ + */ +class CatRepository extends EntityRepository +{ +} + +/** @return array */ +function getResultAsEntities(CatRepository $catRepository): array +{ + return $catRepository->createQueryBuilder('c')->getQuery()->getResult(); +} + +function getOneOrNullEntity(CatRepository $catRepository): Cat|null +{ + return $catRepository->createQueryBuilder('c')->getQuery()->getOneOrNullResult(); +} + +function getSingleEntity(CatRepository $catRepository): Cat +{ + return $catRepository->createQueryBuilder('c')->getQuery()->getSingleResult(); +} + +/** + * Once QueryBuilder::select is called, all results will be mixed. User must manually assert returned type. + * + * @see QueryBuilder::select() + */ +function getMixedResults(CatRepository $catRepository): mixed +{ + return $catRepository->createQueryBuilder('c') + ->select('c.id') + ->getQuery()->getResult(); +} + +function getMixedOrNullResult(CatRepository $catRepository): mixed +{ + return $catRepository->createQueryBuilder('c') + ->select('c.id') + ->getQuery()->getOneOrNullResult(); +} + +function getMixedResult(CatRepository $catRepository): mixed +{ + return $catRepository->createQueryBuilder('c') + ->select('c.id') + ->getQuery()->getSingleResult(); +}