Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8d9b685

Browse files
committedJan 7, 2023
Improve QueryResultDynamicReturnTypeExtension
1 parent e1437a2 commit 8d9b685

File tree

2 files changed

+476
-31
lines changed

2 files changed

+476
-31
lines changed
 

‎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;
2224
use PHPStan\Type\TypeWithClassName;
25+
use PHPStan\Type\TypeTraverser;
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

‎tests/Type/Doctrine/data/QueryResult/queryResult.php

+356-10
Original file line numberDiff line numberDiff line change
@@ -143,47 +143,393 @@ public function testReturnTypeOfQueryMethodsWithExplicitObjectHydrationMode(Enti
143143
}
144144

145145
/**
146-
* Test that we properly infer the return type of Query methods with explicit hydration mode that is not HYDRATE_OBJECT
146+
* Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_ARRAY
147147
*
148-
* We are never able to infer the return type here
148+
* We can infer the return type by changing every object by an array
149149
*/
150-
public function testReturnTypeOfQueryMethodsWithExplicitNonObjectHydrationMode(EntityManagerInterface $em): void
150+
public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(EntityManagerInterface $em): void
151151
{
152152
$query = $em->createQuery('
153153
SELECT m
154154
FROM QueryResult\Entities\Many m
155155
');
156156

157157
assertType(
158-
'mixed',
158+
'array<array>',
159159
$query->getResult(AbstractQuery::HYDRATE_ARRAY)
160160
);
161161
assertType(
162-
'iterable',
162+
'array<array>',
163+
$query->getArrayResult()
164+
);
165+
assertType(
166+
'iterable<array>',
163167
$query->toIterable([], AbstractQuery::HYDRATE_ARRAY)
164168
);
165169
assertType(
166-
'mixed',
170+
'array<array>',
167171
$query->execute(null, AbstractQuery::HYDRATE_ARRAY)
168172
);
169173
assertType(
170-
'mixed',
174+
'array<array>',
171175
$query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY)
172176
);
173177
assertType(
174-
'mixed',
178+
'array<array>',
175179
$query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY)
176180
);
177181
assertType(
178-
'mixed',
182+
'array',
179183
$query->getSingleResult(AbstractQuery::HYDRATE_ARRAY)
180184
);
181185
assertType(
182-
'mixed',
186+
'array|null',
187+
$query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY)
188+
);
189+
190+
$query = $em->createQuery('
191+
SELECT m.intColumn, m.stringNullColumn
192+
FROM QueryResult\Entities\Many m
193+
');
194+
195+
assertType(
196+
'array<array{intColumn: int, stringNullColumn: string|null}>',
197+
$query->getResult(AbstractQuery::HYDRATE_ARRAY)
198+
);
199+
assertType(
200+
'array<array{intColumn: int, stringNullColumn: string|null}>',
201+
$query->getArrayResult()
202+
);
203+
assertType(
204+
'array<array{intColumn: int, stringNullColumn: string|null}>',
205+
$query->execute(null, AbstractQuery::HYDRATE_ARRAY)
206+
);
207+
assertType(
208+
'array<array{intColumn: int, stringNullColumn: string|null}>',
209+
$query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY)
210+
);
211+
assertType(
212+
'array<array{intColumn: int, stringNullColumn: string|null}>',
213+
$query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY)
214+
);
215+
assertType(
216+
'array{intColumn: int, stringNullColumn: string|null}',
217+
$query->getSingleResult(AbstractQuery::HYDRATE_ARRAY)
218+
);
219+
assertType(
220+
'array{intColumn: int, stringNullColumn: string|null}|null',
183221
$query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY)
184222
);
185223
}
186224

225+
/**
226+
* Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR
227+
*/
228+
public function testReturnTypeOfQueryMethodsWithExplicitScalarHydrationMode(EntityManagerInterface $em): void
229+
{
230+
$query = $em->createQuery('
231+
SELECT m
232+
FROM QueryResult\Entities\Many m
233+
');
234+
235+
assertType(
236+
'array<array>',
237+
$query->getResult(AbstractQuery::HYDRATE_SCALAR)
238+
);
239+
assertType(
240+
'array<array>',
241+
$query->getScalarResult()
242+
);
243+
assertType(
244+
'iterable<array>',
245+
$query->toIterable([], AbstractQuery::HYDRATE_SCALAR)
246+
);
247+
assertType(
248+
'array<array>',
249+
$query->execute(null, AbstractQuery::HYDRATE_SCALAR)
250+
);
251+
assertType(
252+
'array<array>',
253+
$query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR)
254+
);
255+
assertType(
256+
'array<array>',
257+
$query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR)
258+
);
259+
assertType(
260+
'array',
261+
$query->getSingleResult(AbstractQuery::HYDRATE_SCALAR)
262+
);
263+
assertType(
264+
'array|null',
265+
$query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR)
266+
);
267+
268+
$query = $em->createQuery('
269+
SELECT m.intColumn, m.stringNullColumn
270+
FROM QueryResult\Entities\Many m
271+
');
272+
273+
assertType(
274+
'array<array{intColumn: int, stringNullColumn: string|null}>',
275+
$query->getResult(AbstractQuery::HYDRATE_SCALAR)
276+
);
277+
assertType(
278+
'array<array{intColumn: int, stringNullColumn: string|null}>',
279+
$query->getScalarResult()
280+
);
281+
assertType(
282+
'array<array{intColumn: int, stringNullColumn: string|null}>',
283+
$query->execute(null, AbstractQuery::HYDRATE_SCALAR)
284+
);
285+
assertType(
286+
'array<array{intColumn: int, stringNullColumn: string|null}>',
287+
$query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR)
288+
);
289+
assertType(
290+
'array<array{intColumn: int, stringNullColumn: string|null}>',
291+
$query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR)
292+
);
293+
assertType(
294+
'array{intColumn: int, stringNullColumn: string|null}',
295+
$query->getSingleResult(AbstractQuery::HYDRATE_SCALAR)
296+
);
297+
assertType(
298+
'array{intColumn: int, stringNullColumn: string|null}|null',
299+
$query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR)
300+
);
301+
}
302+
303+
/**
304+
* Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR
305+
*/
306+
public function testReturnTypeOfQueryMethodsWithExplicitSingleScalarHydrationMode(EntityManagerInterface $em): void
307+
{
308+
$query = $em->createQuery('
309+
SELECT m
310+
FROM QueryResult\Entities\Many m
311+
');
312+
313+
assertType(
314+
'array<array>',
315+
$query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR)
316+
);
317+
assertType(
318+
'array<array>',
319+
$query->getSingleScalarResult()
320+
);
321+
assertType(
322+
'iterable<array>',
323+
$query->toIterable([], AbstractQuery::HYDRATE_SINGLE_SCALAR)
324+
);
325+
assertType(
326+
'array<array>',
327+
$query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR)
328+
);
329+
assertType(
330+
'array<array>',
331+
$query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR)
332+
);
333+
assertType(
334+
'array<array>',
335+
$query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR)
336+
);
337+
assertType(
338+
'array',
339+
$query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR)
340+
);
341+
assertType(
342+
'array|null',
343+
$query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR)
344+
);
345+
346+
$query = $em->createQuery('
347+
SELECT m.intColumn
348+
FROM QueryResult\Entities\Many m
349+
');
350+
351+
assertType(
352+
'array<array{intColumn: int}>',
353+
$query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR)
354+
);
355+
assertType(
356+
'array<array{intColumn: int}>',
357+
$query->getSingleScalarResult()
358+
);
359+
assertType(
360+
'array<array{intColumn: int}>',
361+
$query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR)
362+
);
363+
assertType(
364+
'array<array{intColumn: int}>',
365+
$query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR)
366+
);
367+
assertType(
368+
'array<array{intColumn: int}>',
369+
$query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR)
370+
);
371+
assertType(
372+
'array{intColumn: int}',
373+
$query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR)
374+
);
375+
assertType(
376+
'array{intColumn: int}|null',
377+
$query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR)
378+
);
379+
}
380+
381+
/**
382+
* Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SIMPLEOBJECT
383+
*
384+
* We are never able to infer the return type here
385+
*/
386+
public function testReturnTypeOfQueryMethodsWithExplicitSimpleObjectHydrationMode(EntityManagerInterface $em): void
387+
{
388+
$query = $em->createQuery('
389+
SELECT m
390+
FROM QueryResult\Entities\Many m
391+
');
392+
393+
assertType(
394+
'array<QueryResult\Entities\Many>',
395+
$query->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT)
396+
);
397+
assertType(
398+
'iterable<QueryResult\Entities\Many>',
399+
$query->toIterable([], AbstractQuery::HYDRATE_SIMPLEOBJECT)
400+
);
401+
assertType(
402+
'array<QueryResult\Entities\Many>',
403+
$query->execute(null, AbstractQuery::HYDRATE_SIMPLEOBJECT)
404+
);
405+
assertType(
406+
'array<QueryResult\Entities\Many>',
407+
$query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT)
408+
);
409+
assertType(
410+
'array<QueryResult\Entities\Many>',
411+
$query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT)
412+
);
413+
assertType(
414+
'QueryResult\Entities\Many',
415+
$query->getSingleResult(AbstractQuery::HYDRATE_SIMPLEOBJECT)
416+
);
417+
assertType(
418+
'QueryResult\Entities\Many|null',
419+
$query->getOneOrNullResult(AbstractQuery::HYDRATE_SIMPLEOBJECT)
420+
);
421+
422+
$query = $em->createQuery('
423+
SELECT m.intColumn, m.stringNullColumn
424+
FROM QueryResult\Entities\Many m
425+
');
426+
427+
assertType(
428+
'array',
429+
$query->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT)
430+
);
431+
assertType(
432+
'array',
433+
$query->execute(null, AbstractQuery::HYDRATE_SIMPLEOBJECT)
434+
);
435+
assertType(
436+
'array',
437+
$query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT)
438+
);
439+
assertType(
440+
'array',
441+
$query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT)
442+
);
443+
assertType(
444+
'mixed',
445+
$query->getSingleResult(AbstractQuery::HYDRATE_SIMPLEOBJECT)
446+
);
447+
assertType(
448+
'mixed',
449+
$query->getOneOrNullResult(AbstractQuery::HYDRATE_SIMPLEOBJECT)
450+
);
451+
}
452+
453+
/**
454+
* Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR_COLUMN
455+
*
456+
* We are never able to infer the return type here
457+
*/
458+
public function testReturnTypeOfQueryMethodsWithExplicitScalarColumnHydrationMode(EntityManagerInterface $em): void
459+
{
460+
$query = $em->createQuery('
461+
SELECT m
462+
FROM QueryResult\Entities\Many m
463+
');
464+
465+
assertType(
466+
'array',
467+
$query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN)
468+
);
469+
assertType(
470+
'array',
471+
$query->getSingleColumnResult()
472+
);
473+
assertType(
474+
'iterable',
475+
$query->toIterable([], AbstractQuery::HYDRATE_SCALAR_COLUMN)
476+
);
477+
assertType(
478+
'array',
479+
$query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN)
480+
);
481+
assertType(
482+
'array',
483+
$query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN)
484+
);
485+
assertType(
486+
'array',
487+
$query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN)
488+
);
489+
assertType(
490+
'mixed',
491+
$query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN)
492+
);
493+
assertType(
494+
'mixed',
495+
$query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN)
496+
);
497+
498+
$query = $em->createQuery('
499+
SELECT m.intColumn
500+
FROM QueryResult\Entities\Many m
501+
');
502+
503+
assertType(
504+
'array<int>',
505+
$query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN)
506+
);
507+
assertType(
508+
'array<int>',
509+
$query->getSingleColumnResult()
510+
);
511+
assertType(
512+
'array<int>',
513+
$query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN)
514+
);
515+
assertType(
516+
'array<int>',
517+
$query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN)
518+
);
519+
assertType(
520+
'array<int>',
521+
$query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN)
522+
);
523+
assertType(
524+
'int',
525+
$query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN)
526+
);
527+
assertType(
528+
'int|null',
529+
$query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN)
530+
);
531+
}
532+
187533
/**
188534
* Test that we properly infer the return type of Query methods with explicit hydration mode that is not a constant value
189535
*

0 commit comments

Comments
 (0)
Please sign in to comment.