Skip to content

Commit dac5f17

Browse files
committed
Add DynamicReturnTypeExtension for Query::getResult() and variants
1 parent e444a78 commit dac5f17

File tree

4 files changed

+393
-0
lines changed

4 files changed

+393
-0
lines changed

extension.neon

+4
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ services:
127127
objectMetadataResolver: @PHPStan\Type\Doctrine\ObjectMetadataResolver
128128
tags:
129129
- phpstan.broker.dynamicMethodReturnTypeExtension
130+
-
131+
class: PHPStan\Type\Doctrine\Query\QueryResultDynamicReturnTypeExtension
132+
tags:
133+
- phpstan.broker.dynamicMethodReturnTypeExtension
130134
-
131135
class: PHPStan\Type\Doctrine\QueryBuilder\Expr\ExpressionBuilderDynamicReturnTypeExtension
132136
arguments:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Query;
4+
5+
use Doctrine\ORM\AbstractQuery;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use PHPStan\ShouldNotHappenException;
11+
use PHPStan\Type\ArrayType;
12+
use PHPStan\Type\Constant\ConstantIntegerType;
13+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
14+
use PHPStan\Type\Generic\GenericObjectType;
15+
use PHPStan\Type\IntegerType;
16+
use PHPStan\Type\MixedType;
17+
use PHPStan\Type\NullType;
18+
use PHPStan\Type\Type;
19+
use PHPStan\Type\TypeCombinator;
20+
use PHPStan\Type\VoidType;
21+
22+
final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
23+
{
24+
25+
private const METHOD_HYDRATION_MODE_ARG = [
26+
'getResult' => 0,
27+
'execute' => 1,
28+
'executeIgnoreQueryCache' => 1,
29+
'executeUsingQueryCache' => 1,
30+
'getOneOrNullResult' => 0,
31+
'getSingleResult' => 0,
32+
];
33+
34+
public function getClass(): string
35+
{
36+
return AbstractQuery::class;
37+
}
38+
39+
public function isMethodSupported(MethodReflection $methodReflection): bool
40+
{
41+
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]);
42+
}
43+
44+
public function getTypeFromMethodCall(
45+
MethodReflection $methodReflection,
46+
MethodCall $methodCall,
47+
Scope $scope
48+
): Type
49+
{
50+
$methodName = $methodReflection->getName();
51+
52+
if (!isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
53+
throw new ShouldNotHappenException();
54+
}
55+
56+
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
57+
$args = $methodCall->getArgs();
58+
59+
if (isset($args[$argIndex])) {
60+
$hydrationMode = $scope->getType($args[$argIndex]->value);
61+
} else {
62+
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
63+
$methodReflection->getVariants()
64+
);
65+
$parameter = $parametersAcceptor->getParameters()[$argIndex];
66+
$hydrationMode = $parameter->getDefaultValue() ?? new NullType();
67+
}
68+
69+
$queryType = $scope->getType($methodCall->var);
70+
$queryResultType = $this->getQueryResultType($queryType);
71+
72+
return $this->getMethodReturnTypeForHydrationMode(
73+
$methodReflection,
74+
$hydrationMode,
75+
$queryResultType
76+
);
77+
}
78+
79+
private function getQueryResultType(Type $queryType): Type
80+
{
81+
if (!$queryType instanceof GenericObjectType) {
82+
return new MixedType();
83+
}
84+
85+
$types = $queryType->getTypes();
86+
87+
return $types[0] ?? new MixedType();
88+
}
89+
90+
private function getMethodReturnTypeForHydrationMode(
91+
MethodReflection $methodReflection,
92+
Type $hydrationMode,
93+
Type $queryResultType
94+
): Type
95+
{
96+
if ($queryResultType instanceof VoidType) {
97+
// A void query result type indicates an UPDATE or DELETE query.
98+
// In this case all methods return the number of affected rows.
99+
return new IntegerType();
100+
}
101+
102+
if (!$this->isObjectHydrationMode($hydrationMode)) {
103+
// We support only HYDRATE_OBJECT. For other hydration modes, we
104+
// return the declared return type of the method.
105+
return $this->originalReturnType($methodReflection);
106+
}
107+
108+
switch ($methodReflection->getName()) {
109+
case 'getSingleResult':
110+
return $queryResultType;
111+
case 'getOneOrNullResult':
112+
return TypeCombinator::addNull($queryResultType);
113+
default:
114+
return new ArrayType(
115+
new MixedType(),
116+
$queryResultType
117+
);
118+
}
119+
}
120+
121+
private function isObjectHydrationMode(Type $type): bool
122+
{
123+
if (!$type instanceof ConstantIntegerType) {
124+
return false;
125+
}
126+
127+
return $type->getValue() === AbstractQuery::HYDRATE_OBJECT;
128+
}
129+
130+
private function originalReturnType(MethodReflection $methodReflection): Type
131+
{
132+
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
133+
$methodReflection->getVariants()
134+
);
135+
136+
return $parametersAcceptor->getReturnType();
137+
}
138+
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Query;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
class QueryResultDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
8+
{
9+
10+
/** @return iterable<mixed> */
11+
public function dataFileAsserts(): iterable
12+
{
13+
yield from $this->gatherAssertTypes(__DIR__ . '/../data/QueryResult/queryResult.php');
14+
}
15+
16+
/**
17+
* @dataProvider dataFileAsserts
18+
* @param string $assertType
19+
* @param string $file
20+
* @param mixed ...$args
21+
*/
22+
public function testFileAsserts(
23+
string $assertType,
24+
string $file,
25+
...$args
26+
): void
27+
{
28+
$this->assertFileAsserts($assertType, $file, ...$args);
29+
}
30+
31+
/** @return string[] */
32+
public static function getAdditionalConfigFiles(): array
33+
{
34+
return [__DIR__ . '/../data/QueryResult/config.neon'];
35+
}
36+
37+
}

0 commit comments

Comments
 (0)