Skip to content

Commit eec9f2e

Browse files
Improve QueryResultDynamicReturnTypeExtension
1 parent f769796 commit eec9f2e

File tree

3 files changed

+478
-33
lines changed

3 files changed

+478
-33
lines changed

Diff for: composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
],
88
"require": {
99
"php": "^7.2 || ^8.0",
10-
"phpstan/phpstan": "^1.8.11"
10+
"phpstan/phpstan": "^1.9.0"
1111
},
1212
"conflict": {
1313
"doctrine/collections": "<1.0",

Diff for: src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php

+121-22
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@
99
use PHPStan\Reflection\ParametersAcceptorSelector;
1010
use PHPStan\ShouldNotHappenException;
1111
use PHPStan\Type\ArrayType;
12+
use PHPStan\Type\Constant\ConstantArrayType;
1213
use PHPStan\Type\Constant\ConstantIntegerType;
1314
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1415
use PHPStan\Type\Generic\GenericObjectType;
1516
use PHPStan\Type\IntegerType;
1617
use PHPStan\Type\IterableType;
1718
use PHPStan\Type\MixedType;
1819
use PHPStan\Type\NullType;
20+
use PHPStan\Type\ObjectWithoutClassType;
1921
use PHPStan\Type\Type;
2022
use PHPStan\Type\TypeCombinator;
23+
use PHPStan\Type\TypeTraverser;
2124
use PHPStan\Type\VoidType;
25+
use function count;
2226

2327
final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
2428
{
@@ -33,14 +37,22 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
3337
'getSingleResult' => 0,
3438
];
3539

40+
private const METHOD_HYDRATION_MODE = [
41+
'getArrayResult' => AbstractQuery::HYDRATE_ARRAY,
42+
'getScalarResult' => AbstractQuery::HYDRATE_SCALAR,
43+
'getSingleColumnResult' => AbstractQuery::HYDRATE_SCALAR_COLUMN,
44+
'getSingleScalarResult' => AbstractQuery::HYDRATE_SINGLE_SCALAR,
45+
];
46+
3647
public function getClass(): string
3748
{
3849
return AbstractQuery::class;
3950
}
4051

4152
public function isMethodSupported(MethodReflection $methodReflection): bool
4253
{
43-
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]);
54+
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()])
55+
|| isset(self::METHOD_HYDRATION_MODE[$methodReflection->getName()]);
4456
}
4557

4658
public function getTypeFromMethodCall(
@@ -51,21 +63,23 @@ public function getTypeFromMethodCall(
5163
{
5264
$methodName = $methodReflection->getName();
5365

54-
if (!isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
55-
throw new ShouldNotHappenException();
56-
}
57-
58-
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
59-
$args = $methodCall->getArgs();
60-
61-
if (isset($args[$argIndex])) {
62-
$hydrationMode = $scope->getType($args[$argIndex]->value);
66+
if (isset(self::METHOD_HYDRATION_MODE[$methodName])) {
67+
$hydrationMode = self::METHOD_HYDRATION_MODE[$methodName];
68+
} elseif (isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
69+
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
70+
$args = $methodCall->getArgs();
71+
72+
if (isset($args[$argIndex])) {
73+
$hydrationMode = $scope->getType($args[$argIndex]->value);
74+
} else {
75+
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
76+
$methodReflection->getVariants()
77+
);
78+
$parameter = $parametersAcceptor->getParameters()[$argIndex];
79+
$hydrationMode = $parameter->getDefaultValue() ?? new NullType();
80+
}
6381
} else {
64-
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
65-
$methodReflection->getVariants()
66-
);
67-
$parameter = $parametersAcceptor->getParameters()[$argIndex];
68-
$hydrationMode = $parameter->getDefaultValue() ?? new NullType();
82+
throw new ShouldNotHappenException();
6983
}
7084

7185
$queryType = $scope->getType($methodCall->var);
@@ -109,12 +123,32 @@ private function getMethodReturnTypeForHydrationMode(
109123
return $this->originalReturnType($methodReflection);
110124
}
111125

112-
if (!$this->isObjectHydrationMode($hydrationMode)) {
113-
// We support only HYDRATE_OBJECT. For other hydration modes, we
114-
// return the declared return type of the method.
126+
if (!$hydrationMode instanceof ConstantIntegerType) {
115127
return $this->originalReturnType($methodReflection);
116128
}
117129

130+
switch ($hydrationMode->getValue()) {
131+
case AbstractQuery::HYDRATE_OBJECT:
132+
break;
133+
case AbstractQuery::HYDRATE_ARRAY:
134+
$queryResultType = $this->getArrayHydratedReturnType($queryResultType);
135+
break;
136+
case AbstractQuery::HYDRATE_SCALAR:
137+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
138+
break;
139+
case AbstractQuery::HYDRATE_SINGLE_SCALAR:
140+
$queryResultType = $this->getSingleScalarHydratedReturnType($queryResultType);
141+
break;
142+
case AbstractQuery::HYDRATE_SIMPLEOBJECT:
143+
$queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType);
144+
break;
145+
case AbstractQuery::HYDRATE_SCALAR_COLUMN:
146+
$queryResultType = $this->getScalarColumnHydratedReturnType($queryResultType);
147+
break;
148+
default:
149+
return $this->originalReturnType($methodReflection);
150+
}
151+
118152
switch ($methodReflection->getName()) {
119153
case 'getSingleResult':
120154
return $queryResultType;
@@ -133,13 +167,78 @@ private function getMethodReturnTypeForHydrationMode(
133167
}
134168
}
135169

136-
private function isObjectHydrationMode(Type $type): bool
170+
private function getArrayHydratedReturnType(Type $queryResultType): Type
171+
{
172+
return TypeTraverser::map(
173+
$queryResultType,
174+
static function (Type $type, callable $traverse): Type {
175+
$isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type);
176+
if ($isObject->yes()) {
177+
return new ArrayType(new MixedType(), new MixedType());
178+
}
179+
if ($isObject->maybe()) {
180+
return new MixedType();
181+
}
182+
183+
return $traverse($type);
184+
}
185+
);
186+
}
187+
188+
private function getScalarHydratedReturnType(Type $queryResultType): Type
137189
{
138-
if (!$type instanceof ConstantIntegerType) {
139-
return false;
190+
if (!$queryResultType instanceof ArrayType) {
191+
return new ArrayType(new MixedType(), new MixedType());
192+
}
193+
194+
$itemType = $queryResultType->getItemType();
195+
$hasNoObject = (new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no();
196+
$hasNoArray = $itemType->isArray()->no();
197+
198+
if ($hasNoArray && $hasNoObject) {
199+
return $queryResultType;
200+
}
201+
202+
return new ArrayType(new MixedType(), new MixedType());
203+
}
204+
205+
private function getSimpleObjectHydratedReturnType(Type $queryResultType): Type
206+
{
207+
if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) {
208+
return $queryResultType;
209+
}
210+
211+
return new MixedType();
212+
}
213+
214+
private function getSingleScalarHydratedReturnType(Type $queryResultType): Type
215+
{
216+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
217+
if (!$queryResultType instanceof ConstantArrayType) {
218+
return new ArrayType(new MixedType(), new MixedType());
219+
}
220+
221+
$values = $queryResultType->getValueTypes();
222+
if (count($values) !== 1) {
223+
return new ArrayType(new MixedType(), new MixedType());
224+
}
225+
226+
return $queryResultType;
227+
}
228+
229+
private function getScalarColumnHydratedReturnType(Type $queryResultType): Type
230+
{
231+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
232+
if (!$queryResultType instanceof ConstantArrayType) {
233+
return new MixedType();
234+
}
235+
236+
$values = $queryResultType->getValueTypes();
237+
if (count($values) !== 1) {
238+
return new MixedType();
140239
}
141240

142-
return $type->getValue() === AbstractQuery::HYDRATE_OBJECT;
241+
return $queryResultType->getFirstIterableValueType();
143242
}
144243

145244
private function originalReturnType(MethodReflection $methodReflection): Type

0 commit comments

Comments
 (0)