Skip to content

Commit 3ada067

Browse files
committed
Support for infering the result type of queries in EntityManager::createQuery()
Adds a type parameter "TResult" on the Doctrine\ORM\Query and Doctrine\ORM\AbstractQuery stubs. This parameter represents the type of the results returned by the getResult() and execute() family of methods, when in HYDRATE_OBJECT mode. Also adds a DynamicReturnTypeExtension on EntityManager::createQuery() so that TResult is infered from the query string. Caveat: As the hydration mode influences the return type of the getResult() and execute() family of methods, we can not just declare that these methods return TResult yet. This will require a DynamicReturnTypeExtension.
1 parent 6c7d28a commit 3ada067

21 files changed

+2488
-6
lines changed

extension.neon

+7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ parameters:
3838
- stubs/ORM/AbstractQuery.stub
3939
- stubs/ORM/Mapping/ClassMetadata.stub
4040
- stubs/ORM/Mapping/ClassMetadataInfo.stub
41+
- stubs/ORM/Query.stub
4142
- stubs/Persistence/Mapping/ClassMetadata.stub
4243
- stubs/ServiceDocumentRepository.stub
4344

@@ -120,6 +121,12 @@ services:
120121
class: PHPStan\Type\Doctrine\Query\QueryGetDqlDynamicReturnTypeExtension
121122
tags:
122123
- phpstan.broker.dynamicMethodReturnTypeExtension
124+
-
125+
class: PHPStan\Type\Doctrine\CreateQueryDynamicReturnTypeExtension
126+
arguments:
127+
objectMetadataResolver: @PHPStan\Type\Doctrine\ObjectMetadataResolver
128+
tags:
129+
- phpstan.broker.dynamicMethodReturnTypeExtension
123130
-
124131
class: PHPStan\Type\Doctrine\QueryBuilder\Expr\ExpressionBuilderDynamicReturnTypeExtension
125132
arguments:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine;
4+
5+
use Doctrine\Common\CommonException;
6+
use Doctrine\DBAL\DBALException;
7+
use Doctrine\ORM\EntityManagerInterface;
8+
use Doctrine\ORM\ORMException;
9+
use Doctrine\ORM\Query;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Reflection\MethodReflection;
13+
use PHPStan\Type\Constant\ConstantStringType;
14+
use PHPStan\Type\Doctrine\Query\QueryResultTypeBuilder;
15+
use PHPStan\Type\Doctrine\Query\QueryResultTypeWalker;
16+
use PHPStan\Type\Doctrine\Query\QueryType;
17+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
18+
use PHPStan\Type\Generic\GenericObjectType;
19+
use PHPStan\Type\IntersectionType;
20+
use PHPStan\Type\MixedType;
21+
use PHPStan\Type\Type;
22+
use PHPStan\Type\TypeTraverser;
23+
use PHPStan\Type\UnionType;
24+
25+
/**
26+
* Infers TResult in Query<TResult> on EntityManagerInterface::createQuery()
27+
*/
28+
final class CreateQueryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
29+
{
30+
31+
/** @var ObjectMetadataResolver */
32+
private $objectMetadataResolver;
33+
34+
/** @var DescriptorRegistry */
35+
private $descriptorRegistry;
36+
37+
public function __construct(ObjectMetadataResolver $objectMetadataResolver, DescriptorRegistry $descriptorRegistry)
38+
{
39+
$this->objectMetadataResolver = $objectMetadataResolver;
40+
$this->descriptorRegistry = $descriptorRegistry;
41+
}
42+
43+
public function getClass(): string
44+
{
45+
return EntityManagerInterface::class;
46+
}
47+
48+
public function isMethodSupported(MethodReflection $methodReflection): bool
49+
{
50+
return $methodReflection->getName() === 'createQuery';
51+
}
52+
53+
public function getTypeFromMethodCall(
54+
MethodReflection $methodReflection,
55+
MethodCall $methodCall,
56+
Scope $scope
57+
): Type
58+
{
59+
$queryStringArgIndex = 0;
60+
$args = $methodCall->getArgs();
61+
62+
if (!isset($args[$queryStringArgIndex])) {
63+
return new GenericObjectType(
64+
Query::class,
65+
[new MixedType()]
66+
);
67+
}
68+
69+
$argType = $scope->getType($args[$queryStringArgIndex]->value);
70+
71+
return TypeTraverser::map($argType, function (Type $type, callable $traverse): Type {
72+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
73+
return $traverse($type);
74+
}
75+
if ($type instanceof ConstantStringType) {
76+
$queryString = $type->getValue();
77+
78+
$em = $this->objectMetadataResolver->getObjectManager();
79+
if (!$em instanceof EntityManagerInterface) {
80+
return new QueryType($queryString, null);
81+
}
82+
83+
$typeBuilder = new QueryResultTypeBuilder();
84+
85+
try {
86+
$query = $em->createQuery($queryString);
87+
QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry);
88+
} catch (ORMException | DBALException | CommonException $e) {
89+
return new QueryType($queryString, null);
90+
}
91+
92+
return new QueryType($queryString, $typeBuilder->getResultType());
93+
}
94+
return new GenericObjectType(
95+
Query::class,
96+
[new MixedType()]
97+
);
98+
});
99+
}
100+
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Query;
4+
5+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
6+
use PHPStan\Type\Constant\ConstantIntegerType;
7+
use PHPStan\Type\Constant\ConstantStringType;
8+
use PHPStan\Type\Type;
9+
use PHPStan\Type\VoidType;
10+
11+
/**
12+
* QueryResultTypeBuilder helps building the result type of a query
13+
*
14+
* Like Doctrine\ORM\Query\ResultSetMapping, but for static typing concerns
15+
*/
16+
final class QueryResultTypeBuilder
17+
{
18+
19+
/** @var bool */
20+
private $selectQuery = false;
21+
22+
/** @var array<array-key,Type> */
23+
private $entities = [];
24+
25+
/** @var array<array-key,Type> */
26+
private $scalars = [];
27+
28+
public function setSelectQuery(): void
29+
{
30+
$this->selectQuery = true;
31+
}
32+
33+
public function isSelectQuery(): bool
34+
{
35+
return $this->selectQuery;
36+
}
37+
38+
/**
39+
* @param array-key $alias
40+
*/
41+
public function addEntity($alias, Type $type): void
42+
{
43+
$this->entities[$alias] = $type;
44+
}
45+
46+
/**
47+
* @return array<array-key,Type>
48+
*/
49+
public function getEntities(): array
50+
{
51+
return $this->entities;
52+
}
53+
54+
/**
55+
* @param array-key $alias
56+
*/
57+
public function addScalar($alias, Type $type): void
58+
{
59+
$this->scalars[$alias] = $type;
60+
}
61+
62+
/**
63+
* @return array<int,Type>
64+
*/
65+
public function getScalars(): array
66+
{
67+
return $this->scalars;
68+
}
69+
70+
public function getResultType(): Type
71+
{
72+
// It makes no sense to speak about a result type for UPDATE or DELETE
73+
// queries, so we return VoidType here.
74+
if (!$this->selectQuery) {
75+
return new VoidType();
76+
}
77+
78+
if (count($this->entities) === 1 && count($this->scalars) === 0) {
79+
foreach ($this->entities as $entityType) {
80+
return $entityType;
81+
}
82+
}
83+
84+
$builder = ConstantArrayTypeBuilder::createEmpty();
85+
86+
foreach ($this->entities as $alias => $entityType) {
87+
$offsetType = $this->resolveOffsetType($alias);
88+
$builder->setOffsetValueType($offsetType, $entityType);
89+
}
90+
91+
foreach ($this->scalars as $alias => $scalarType) {
92+
$offsetType = $this->resolveOffsetType($alias);
93+
$builder->setOffsetValueType($offsetType, $scalarType);
94+
}
95+
96+
return $builder->getArray();
97+
}
98+
99+
/**
100+
* @param array-key $alias
101+
*/
102+
private function resolveOffsetType($alias): Type
103+
{
104+
if (is_int($alias)) {
105+
return new ConstantIntegerType($alias);
106+
}
107+
108+
return new ConstantStringType($alias);
109+
}
110+
111+
}

0 commit comments

Comments
 (0)