Skip to content

Commit 7d7863a

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 9f40e7f commit 7d7863a

16 files changed

+2351
-1
lines changed

extension.neon

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

4444
parametersSchema:
@@ -120,6 +120,12 @@ services:
120120
class: PHPStan\Type\Doctrine\Query\QueryGetDqlDynamicReturnTypeExtension
121121
tags:
122122
- phpstan.broker.dynamicMethodReturnTypeExtension
123+
-
124+
class: PHPStan\Type\Doctrine\CreateQueryDynamicReturnTypeExtension
125+
arguments:
126+
objectMetadataResolver: @PHPStan\Type\Doctrine\ObjectMetadataResolver
127+
tags:
128+
- phpstan.broker.dynamicMethodReturnTypeExtension
123129
-
124130
class: PHPStan\Type\Doctrine\QueryBuilder\Expr\ExpressionBuilderDynamicReturnTypeExtension
125131
arguments:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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\DynamicMethodReturnTypeExtension;
17+
use PHPStan\Type\Generic\GenericObjectType;
18+
use PHPStan\Type\MixedType;
19+
use PHPStan\Type\Type;
20+
21+
/**
22+
* Infers TResult in Query<TResult> on EntityManagerInterface::createQuery()
23+
*/
24+
final class CreateQueryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
25+
{
26+
27+
/** @var ObjectMetadataResolver */
28+
private $objectMetadataResolver;
29+
30+
/** @var DescriptorRegistry */
31+
private $descriptorRegistry;
32+
33+
public function __construct(ObjectMetadataResolver $objectMetadataResolver, DescriptorRegistry $descriptorRegistry)
34+
{
35+
$this->objectMetadataResolver = $objectMetadataResolver;
36+
$this->descriptorRegistry = $descriptorRegistry;
37+
}
38+
39+
public function getClass(): string
40+
{
41+
return EntityManagerInterface::class;
42+
}
43+
44+
public function isMethodSupported(MethodReflection $methodReflection): bool
45+
{
46+
return $methodReflection->getName() === 'createQuery';
47+
}
48+
49+
public function getTypeFromMethodCall(
50+
MethodReflection $methodReflection,
51+
MethodCall $methodCall,
52+
Scope $scope
53+
): Type
54+
{
55+
$queryStringArgIndex = 0;
56+
$args = $methodCall->getArgs();
57+
58+
if (!isset($args[$queryStringArgIndex])) {
59+
return $this->fallbackType();
60+
}
61+
62+
$argType = $scope->getType($args[$queryStringArgIndex]->value);
63+
if (!$argType instanceof ConstantStringType) {
64+
return $this->fallbackType();
65+
}
66+
67+
$queryString = $argType->getValue();
68+
69+
$em = $this->objectMetadataResolver->getObjectManager();
70+
if (!$em instanceof EntityManagerInterface) {
71+
return $this->fallbackType();
72+
}
73+
74+
$typeBuilder = new QueryResultTypeBuilder();
75+
76+
try {
77+
$query = $em->createQuery($queryString);
78+
QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry);
79+
} catch (ORMException | DBALException | CommonException $e) {
80+
return $this->fallbackType();
81+
}
82+
83+
return new GenericObjectType(
84+
Query::class,
85+
[$typeBuilder->getResultType()]
86+
);
87+
}
88+
89+
private function fallbackType(): GenericObjectType
90+
{
91+
return new GenericObjectType(
92+
Query::class,
93+
[new MixedType()]
94+
);
95+
}
96+
97+
}
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)