Skip to content

Commit 03741b4

Browse files
VincentLangletondrejmirtes
authored andcommitted
Improve QueryResultDynamicReturnTypeExtension
1 parent 11b14c4 commit 03741b4

File tree

2 files changed

+537
-31
lines changed

2 files changed

+537
-31
lines changed

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

+147-21
Original file line numberDiff line numberDiff line change
@@ -10,14 +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;
15+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
1416
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1517
use PHPStan\Type\IntegerType;
1618
use PHPStan\Type\IterableType;
19+
use PHPStan\Type\MixedType;
1720
use PHPStan\Type\NullType;
21+
use PHPStan\Type\ObjectWithoutClassType;
1822
use PHPStan\Type\Type;
1923
use PHPStan\Type\TypeCombinator;
24+
use PHPStan\Type\TypeTraverser;
25+
use PHPStan\Type\TypeWithClassName;
2026
use PHPStan\Type\VoidType;
27+
use function count;
2128

2229
final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
2330
{
@@ -32,14 +39,32 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
3239
'getSingleResult' => 0,
3340
];
3441

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+
49+
/** @var ObjectMetadataResolver */
50+
private $objectMetadataResolver;
51+
52+
public function __construct(
53+
ObjectMetadataResolver $objectMetadataResolver
54+
)
55+
{
56+
$this->objectMetadataResolver = $objectMetadataResolver;
57+
}
58+
3559
public function getClass(): string
3660
{
3761
return AbstractQuery::class;
3862
}
3963

4064
public function isMethodSupported(MethodReflection $methodReflection): bool
4165
{
42-
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]);
66+
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()])
67+
|| isset(self::METHOD_HYDRATION_MODE[$methodReflection->getName()]);
4368
}
4469

4570
public function getTypeFromMethodCall(
@@ -50,21 +75,23 @@ public function getTypeFromMethodCall(
5075
{
5176
$methodName = $methodReflection->getName();
5277

53-
if (!isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
54-
throw new ShouldNotHappenException();
55-
}
56-
57-
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
58-
$args = $methodCall->getArgs();
78+
if (isset(self::METHOD_HYDRATION_MODE[$methodName])) {
79+
$hydrationMode = new ConstantIntegerType(self::METHOD_HYDRATION_MODE[$methodName]);
80+
} elseif (isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
81+
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
82+
$args = $methodCall->getArgs();
5983

60-
if (isset($args[$argIndex])) {
61-
$hydrationMode = $scope->getType($args[$argIndex]->value);
84+
if (isset($args[$argIndex])) {
85+
$hydrationMode = $scope->getType($args[$argIndex]->value);
86+
} else {
87+
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
88+
$methodReflection->getVariants()
89+
);
90+
$parameter = $parametersAcceptor->getParameters()[$argIndex];
91+
$hydrationMode = $parameter->getDefaultValue() ?? new NullType();
92+
}
6293
} else {
63-
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
64-
$methodReflection->getVariants()
65-
);
66-
$parameter = $parametersAcceptor->getParameters()[$argIndex];
67-
$hydrationMode = $parameter->getDefaultValue() ?? new NullType();
94+
throw new ShouldNotHappenException();
6895
}
6996

7097
$queryType = $scope->getType($methodCall->var);
@@ -98,12 +125,34 @@ private function getMethodReturnTypeForHydrationMode(
98125
return $this->originalReturnType($methodReflection);
99126
}
100127

101-
if (!$this->isObjectHydrationMode($hydrationMode)) {
102-
// We support only HYDRATE_OBJECT. For other hydration modes, we
103-
// return the declared return type of the method.
128+
if (!$hydrationMode instanceof ConstantIntegerType) {
104129
return $this->originalReturnType($methodReflection);
105130
}
106131

132+
$singleResult = false;
133+
switch ($hydrationMode->getValue()) {
134+
case AbstractQuery::HYDRATE_OBJECT:
135+
break;
136+
case AbstractQuery::HYDRATE_ARRAY:
137+
$queryResultType = $this->getArrayHydratedReturnType($queryResultType);
138+
break;
139+
case AbstractQuery::HYDRATE_SCALAR:
140+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
141+
break;
142+
case AbstractQuery::HYDRATE_SINGLE_SCALAR:
143+
$singleResult = true;
144+
$queryResultType = $this->getSingleScalarHydratedReturnType($queryResultType);
145+
break;
146+
case AbstractQuery::HYDRATE_SIMPLEOBJECT:
147+
$queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType);
148+
break;
149+
case AbstractQuery::HYDRATE_SCALAR_COLUMN:
150+
$queryResultType = $this->getScalarColumnHydratedReturnType($queryResultType);
151+
break;
152+
default:
153+
return $this->originalReturnType($methodReflection);
154+
}
155+
107156
switch ($methodReflection->getName()) {
108157
case 'getSingleResult':
109158
return $queryResultType;
@@ -115,6 +164,10 @@ private function getMethodReturnTypeForHydrationMode(
115164
$queryResultType
116165
);
117166
default:
167+
if ($singleResult) {
168+
return $queryResultType;
169+
}
170+
118171
if ($queryKeyType->isNull()->yes()) {
119172
return AccessoryArrayListType::intersectWith(new ArrayType(
120173
new IntegerType(),
@@ -128,13 +181,86 @@ private function getMethodReturnTypeForHydrationMode(
128181
}
129182
}
130183

131-
private function isObjectHydrationMode(Type $type): bool
184+
private function getArrayHydratedReturnType(Type $queryResultType): Type
185+
{
186+
$objectManager = $this->objectMetadataResolver->getObjectManager();
187+
188+
return TypeTraverser::map(
189+
$queryResultType,
190+
static function (Type $type, callable $traverse) use ($objectManager): Type {
191+
$isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type);
192+
if ($isObject->no()) {
193+
return $traverse($type);
194+
}
195+
if (
196+
$isObject->maybe()
197+
|| !$type instanceof TypeWithClassName
198+
|| $objectManager === null
199+
) {
200+
return new MixedType();
201+
}
202+
203+
return $objectManager->getMetadataFactory()->hasMetadataFor($type->getClassName())
204+
? new ArrayType(new MixedType(), new MixedType())
205+
: $traverse($type);
206+
}
207+
);
208+
}
209+
210+
private function getScalarHydratedReturnType(Type $queryResultType): Type
211+
{
212+
if (!$queryResultType instanceof ArrayType) {
213+
return new ArrayType(new MixedType(), new MixedType());
214+
}
215+
216+
$itemType = $queryResultType->getItemType();
217+
$hasNoObject = (new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no();
218+
$hasNoArray = $itemType->isArray()->no();
219+
220+
if ($hasNoArray && $hasNoObject) {
221+
return $queryResultType;
222+
}
223+
224+
return new ArrayType(new MixedType(), new MixedType());
225+
}
226+
227+
private function getSimpleObjectHydratedReturnType(Type $queryResultType): Type
132228
{
133-
if (!$type instanceof ConstantIntegerType) {
134-
return false;
229+
if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) {
230+
return $queryResultType;
231+
}
232+
233+
return new MixedType();
234+
}
235+
236+
private function getSingleScalarHydratedReturnType(Type $queryResultType): Type
237+
{
238+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
239+
if (!$queryResultType instanceof ConstantArrayType) {
240+
return new MixedType();
241+
}
242+
243+
$values = $queryResultType->getValueTypes();
244+
if (count($values) !== 1) {
245+
return new MixedType();
246+
}
247+
248+
return $queryResultType->getFirstIterableValueType();
249+
}
250+
251+
private function getScalarColumnHydratedReturnType(Type $queryResultType): Type
252+
{
253+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
254+
if (!$queryResultType instanceof ConstantArrayType) {
255+
return new MixedType();
256+
}
257+
258+
$values = $queryResultType->getValueTypes();
259+
if (count($values) !== 1) {
260+
return new MixedType();
135261
}
136262

137-
return $type->getValue() === AbstractQuery::HYDRATE_OBJECT;
263+
return $queryResultType->getFirstIterableValueType();
138264
}
139265

140266
private function originalReturnType(MethodReflection $methodReflection): Type

0 commit comments

Comments
 (0)