Skip to content

Commit c96764c

Browse files
Improve QueryResultDynamicReturnTypeExtension
1 parent e1437a2 commit c96764c

File tree

2 files changed

+476
-31
lines changed

2 files changed

+476
-31
lines changed

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

+120-21
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,21 @@
1010
use PHPStan\ShouldNotHappenException;
1111
use PHPStan\Type\Accessory\AccessoryArrayListType;
1212
use PHPStan\Type\ArrayType;
13+
use PHPStan\Type\Constant\ConstantArrayType;
1314
use PHPStan\Type\Constant\ConstantIntegerType;
1415
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1516
use PHPStan\Type\GenericTypeVariableResolver;
1617
use PHPStan\Type\IntegerType;
1718
use PHPStan\Type\IterableType;
1819
use PHPStan\Type\MixedType;
1920
use PHPStan\Type\NullType;
21+
use PHPStan\Type\ObjectWithoutClassType;
2022
use PHPStan\Type\Type;
2123
use PHPStan\Type\TypeCombinator;
24+
use PHPStan\Type\TypeTraverser;
2225
use PHPStan\Type\TypeWithClassName;
2326
use PHPStan\Type\VoidType;
27+
use function count;
2428

2529
final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
2630
{
@@ -35,14 +39,22 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
3539
'getSingleResult' => 0,
3640
];
3741

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

4354
public function isMethodSupported(MethodReflection $methodReflection): bool
4455
{
45-
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]);
56+
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()])
57+
|| isset(self::METHOD_HYDRATION_MODE[$methodReflection->getName()]);
4658
}
4759

4860
public function getTypeFromMethodCall(
@@ -53,21 +65,23 @@ public function getTypeFromMethodCall(
5365
{
5466
$methodName = $methodReflection->getName();
5567

56-
if (!isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
57-
throw new ShouldNotHappenException();
58-
}
59-
60-
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
61-
$args = $methodCall->getArgs();
68+
if (isset(self::METHOD_HYDRATION_MODE[$methodName])) {
69+
$hydrationMode = new ConstantIntegerType(self::METHOD_HYDRATION_MODE[$methodName]);
70+
} elseif (isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
71+
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
72+
$args = $methodCall->getArgs();
6273

63-
if (isset($args[$argIndex])) {
64-
$hydrationMode = $scope->getType($args[$argIndex]->value);
74+
if (isset($args[$argIndex])) {
75+
$hydrationMode = $scope->getType($args[$argIndex]->value);
76+
} else {
77+
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
78+
$methodReflection->getVariants()
79+
);
80+
$parameter = $parametersAcceptor->getParameters()[$argIndex];
81+
$hydrationMode = $parameter->getDefaultValue() ?? new NullType();
82+
}
6583
} else {
66-
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
67-
$methodReflection->getVariants()
68-
);
69-
$parameter = $parametersAcceptor->getParameters()[$argIndex];
70-
$hydrationMode = $parameter->getDefaultValue() ?? new NullType();
84+
throw new ShouldNotHappenException();
7185
}
7286

7387
$queryType = $scope->getType($methodCall->var);
@@ -131,12 +145,32 @@ private function getMethodReturnTypeForHydrationMode(
131145
return $this->originalReturnType($methodReflection);
132146
}
133147

134-
if (!$this->isObjectHydrationMode($hydrationMode)) {
135-
// We support only HYDRATE_OBJECT. For other hydration modes, we
136-
// return the declared return type of the method.
148+
if (!$hydrationMode instanceof ConstantIntegerType) {
137149
return $this->originalReturnType($methodReflection);
138150
}
139151

152+
switch ($hydrationMode->getValue()) {
153+
case AbstractQuery::HYDRATE_OBJECT:
154+
break;
155+
case AbstractQuery::HYDRATE_ARRAY:
156+
$queryResultType = $this->getArrayHydratedReturnType($queryResultType);
157+
break;
158+
case AbstractQuery::HYDRATE_SCALAR:
159+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
160+
break;
161+
case AbstractQuery::HYDRATE_SINGLE_SCALAR:
162+
$queryResultType = $this->getSingleScalarHydratedReturnType($queryResultType);
163+
break;
164+
case AbstractQuery::HYDRATE_SIMPLEOBJECT:
165+
$queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType);
166+
break;
167+
case AbstractQuery::HYDRATE_SCALAR_COLUMN:
168+
$queryResultType = $this->getScalarColumnHydratedReturnType($queryResultType);
169+
break;
170+
default:
171+
return $this->originalReturnType($methodReflection);
172+
}
173+
140174
switch ($methodReflection->getName()) {
141175
case 'getSingleResult':
142176
return $queryResultType;
@@ -161,13 +195,78 @@ private function getMethodReturnTypeForHydrationMode(
161195
}
162196
}
163197

164-
private function isObjectHydrationMode(Type $type): bool
198+
private function getArrayHydratedReturnType(Type $queryResultType): Type
165199
{
166-
if (!$type instanceof ConstantIntegerType) {
167-
return false;
200+
return TypeTraverser::map(
201+
$queryResultType,
202+
static function (Type $type, callable $traverse): Type {
203+
$isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type);
204+
if ($isObject->yes()) {
205+
return new ArrayType(new MixedType(), new MixedType());
206+
}
207+
if ($isObject->maybe()) {
208+
return new MixedType();
209+
}
210+
211+
return $traverse($type);
212+
}
213+
);
214+
}
215+
216+
private function getScalarHydratedReturnType(Type $queryResultType): Type
217+
{
218+
if (!$queryResultType instanceof ArrayType) {
219+
return new ArrayType(new MixedType(), new MixedType());
220+
}
221+
222+
$itemType = $queryResultType->getItemType();
223+
$hasNoObject = (new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no();
224+
$hasNoArray = $itemType->isArray()->no();
225+
226+
if ($hasNoArray && $hasNoObject) {
227+
return $queryResultType;
228+
}
229+
230+
return new ArrayType(new MixedType(), new MixedType());
231+
}
232+
233+
private function getSimpleObjectHydratedReturnType(Type $queryResultType): Type
234+
{
235+
if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) {
236+
return $queryResultType;
237+
}
238+
239+
return new MixedType();
240+
}
241+
242+
private function getSingleScalarHydratedReturnType(Type $queryResultType): Type
243+
{
244+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
245+
if (!$queryResultType instanceof ConstantArrayType) {
246+
return new ArrayType(new MixedType(), new MixedType());
247+
}
248+
249+
$values = $queryResultType->getValueTypes();
250+
if (count($values) !== 1) {
251+
return new ArrayType(new MixedType(), new MixedType());
252+
}
253+
254+
return $queryResultType;
255+
}
256+
257+
private function getScalarColumnHydratedReturnType(Type $queryResultType): Type
258+
{
259+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
260+
if (!$queryResultType instanceof ConstantArrayType) {
261+
return new MixedType();
262+
}
263+
264+
$values = $queryResultType->getValueTypes();
265+
if (count($values) !== 1) {
266+
return new MixedType();
168267
}
169268

170-
return $type->getValue() === AbstractQuery::HYDRATE_OBJECT;
269+
return $queryResultType->getFirstIterableValueType();
171270
}
172271

173272
private function originalReturnType(MethodReflection $methodReflection): Type

0 commit comments

Comments
 (0)