Skip to content

Commit 5745ea6

Browse files
VincentLangletondrejmirtes
authored andcommitted
Improve QueryResultDynamicReturnTypeExtension
1 parent a565fdb commit 5745ea6

File tree

2 files changed

+585
-25
lines changed

2 files changed

+585
-25
lines changed

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

+199-22
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,22 @@
1010
use PHPStan\ShouldNotHappenException;
1111
use PHPStan\Type\Accessory\AccessoryArrayListType;
1212
use PHPStan\Type\ArrayType;
13+
use PHPStan\Type\BenevolentUnionType;
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\TypeUtils;
26+
use PHPStan\Type\TypeWithClassName;
2027
use PHPStan\Type\VoidType;
28+
use function count;
2129

2230
final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
2331
{
@@ -32,14 +40,32 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
3240
'getSingleResult' => 0,
3341
];
3442

43+
private const METHOD_HYDRATION_MODE = [
44+
'getArrayResult' => AbstractQuery::HYDRATE_ARRAY,
45+
'getScalarResult' => AbstractQuery::HYDRATE_SCALAR,
46+
'getSingleColumnResult' => AbstractQuery::HYDRATE_SCALAR_COLUMN,
47+
'getSingleScalarResult' => AbstractQuery::HYDRATE_SINGLE_SCALAR,
48+
];
49+
50+
/** @var ObjectMetadataResolver */
51+
private $objectMetadataResolver;
52+
53+
public function __construct(
54+
ObjectMetadataResolver $objectMetadataResolver
55+
)
56+
{
57+
$this->objectMetadataResolver = $objectMetadataResolver;
58+
}
59+
3560
public function getClass(): string
3661
{
3762
return AbstractQuery::class;
3863
}
3964

4065
public function isMethodSupported(MethodReflection $methodReflection): bool
4166
{
42-
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]);
67+
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()])
68+
|| isset(self::METHOD_HYDRATION_MODE[$methodReflection->getName()]);
4369
}
4470

4571
public function getTypeFromMethodCall(
@@ -50,21 +76,23 @@ public function getTypeFromMethodCall(
5076
{
5177
$methodName = $methodReflection->getName();
5278

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();
79+
if (isset(self::METHOD_HYDRATION_MODE[$methodName])) {
80+
$hydrationMode = new ConstantIntegerType(self::METHOD_HYDRATION_MODE[$methodName]);
81+
} elseif (isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
82+
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
83+
$args = $methodCall->getArgs();
5984

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

7098
$queryType = $scope->getType($methodCall->var);
@@ -98,23 +126,58 @@ private function getMethodReturnTypeForHydrationMode(
98126
return null;
99127
}
100128

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.
129+
if (!$hydrationMode instanceof ConstantIntegerType) {
130+
return null;
131+
}
132+
133+
$singleResult = false;
134+
switch ($hydrationMode->getValue()) {
135+
case AbstractQuery::HYDRATE_OBJECT:
136+
break;
137+
case AbstractQuery::HYDRATE_ARRAY:
138+
$queryResultType = $this->getArrayHydratedReturnType($queryResultType);
139+
break;
140+
case AbstractQuery::HYDRATE_SCALAR:
141+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
142+
break;
143+
case AbstractQuery::HYDRATE_SINGLE_SCALAR:
144+
$singleResult = true;
145+
$queryResultType = $this->getSingleScalarHydratedReturnType($queryResultType);
146+
break;
147+
case AbstractQuery::HYDRATE_SIMPLEOBJECT:
148+
$queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType);
149+
break;
150+
case AbstractQuery::HYDRATE_SCALAR_COLUMN:
151+
$queryResultType = $this->getScalarColumnHydratedReturnType($queryResultType);
152+
break;
153+
default:
154+
return null;
155+
}
156+
157+
if ($queryResultType === null) {
104158
return null;
105159
}
106160

107161
switch ($methodReflection->getName()) {
108162
case 'getSingleResult':
109163
return $queryResultType;
110164
case 'getOneOrNullResult':
111-
return TypeCombinator::addNull($queryResultType);
165+
$nullableQueryResultType = TypeCombinator::addNull($queryResultType);
166+
if ($queryResultType instanceof BenevolentUnionType) {
167+
$nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType);
168+
}
169+
170+
return $nullableQueryResultType;
112171
case 'toIterable':
113172
return new IterableType(
114173
$queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType,
115174
$queryResultType
116175
);
117176
default:
177+
if ($singleResult) {
178+
return $queryResultType;
179+
}
180+
118181
if ($queryKeyType->isNull()->yes()) {
119182
return AccessoryArrayListType::intersectWith(new ArrayType(
120183
new IntegerType(),
@@ -128,13 +191,127 @@ private function getMethodReturnTypeForHydrationMode(
128191
}
129192
}
130193

131-
private function isObjectHydrationMode(Type $type): bool
194+
/**
195+
* When we're array-hydrating object, we're not sure of the shape of the array.
196+
* We could return `new ArrayTyp(new MixedType(), new MixedType())`
197+
* but the lack of precision in the array keys/values would give false positive.
198+
*
199+
* @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934
200+
*/
201+
private function getArrayHydratedReturnType(Type $queryResultType): ?Type
132202
{
133-
if (!$type instanceof ConstantIntegerType) {
134-
return false;
203+
$objectManager = $this->objectMetadataResolver->getObjectManager();
204+
205+
$mixedFound = false;
206+
$queryResultType = TypeTraverser::map(
207+
$queryResultType,
208+
static function (Type $type, callable $traverse) use ($objectManager, &$mixedFound): Type {
209+
$isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type);
210+
if ($isObject->no()) {
211+
return $traverse($type);
212+
}
213+
if (
214+
$isObject->maybe()
215+
|| !$type instanceof TypeWithClassName
216+
|| $objectManager === null
217+
) {
218+
$mixedFound = true;
219+
220+
return new MixedType();
221+
}
222+
223+
/** @var class-string $className */
224+
$className = $type->getClassName();
225+
if (!$objectManager->getMetadataFactory()->hasMetadataFor($className)) {
226+
return $traverse($type);
227+
}
228+
229+
$mixedFound = true;
230+
231+
return new MixedType();
232+
}
233+
);
234+
235+
return $mixedFound ? null : $queryResultType;
236+
}
237+
238+
/**
239+
* When we're scalar-hydrating object, we're not sure of the shape of the array.
240+
* We could return `new ArrayTyp(new MixedType(), new MixedType())`
241+
* but the lack of precision in the array keys/values would give false positive.
242+
*
243+
* @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544
244+
*/
245+
private function getScalarHydratedReturnType(Type $queryResultType): ?Type
246+
{
247+
if (!$queryResultType->isArray()->yes()) {
248+
return null;
249+
}
250+
251+
foreach ($queryResultType->getArrays() as $arrayType) {
252+
$itemType = $arrayType->getItemType();
253+
254+
if (
255+
!(new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no()
256+
|| !$itemType->isArray()->no()
257+
) {
258+
// We could return `new ArrayTyp(new MixedType(), new MixedType())`
259+
// but the lack of precision in the array keys/values would give false positive
260+
// @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544
261+
return null;
262+
}
263+
}
264+
265+
return $queryResultType;
266+
}
267+
268+
private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type
269+
{
270+
if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) {
271+
return $queryResultType;
272+
}
273+
274+
return null;
275+
}
276+
277+
private function getSingleScalarHydratedReturnType(Type $queryResultType): ?Type
278+
{
279+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
280+
if ($queryResultType === null || !$queryResultType->isConstantArray()->yes()) {
281+
return null;
282+
}
283+
284+
$types = [];
285+
foreach ($queryResultType->getConstantArrays() as $constantArrayType) {
286+
$values = $constantArrayType->getValueTypes();
287+
if (count($values) !== 1) {
288+
return null;
289+
}
290+
291+
$types[] = $constantArrayType->getFirstIterableValueType();
292+
}
293+
294+
return TypeCombinator::union(...$types);
295+
}
296+
297+
private function getScalarColumnHydratedReturnType(Type $queryResultType): ?Type
298+
{
299+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
300+
if ($queryResultType === null || !$queryResultType->isConstantArray()->yes()) {
301+
return null;
302+
}
303+
304+
$types = [];
305+
foreach ($queryResultType->getConstantArrays() as $constantArrayType) {
306+
$values = $constantArrayType->getValueTypes();
307+
if (count($values) !== 1) {
308+
return null;
309+
}
310+
311+
$types[] = $constantArrayType->getFirstIterableValueType();
135312
}
136313

137-
return $type->getValue() === AbstractQuery::HYDRATE_OBJECT;
314+
return TypeCombinator::union(...$types);
138315
}
139316

140317
}

0 commit comments

Comments
 (0)