Skip to content

Commit a274af0

Browse files
committed
More tests
1 parent 7c8e586 commit a274af0

File tree

6 files changed

+343
-48
lines changed

6 files changed

+343
-48
lines changed

src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33
namespace PHPStan\Type\Doctrine\Query;
44

55
use Doctrine\ORM\AbstractQuery;
6-
use Doctrine\ORM\Query;
76
use PhpParser\Node\Expr\MethodCall;
87
use PHPStan\Analyser\Scope;
98
use PHPStan\Reflection\MethodReflection;
109
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use PHPStan\ShouldNotHappenException;
1111
use PHPStan\Type\ArrayType;
1212
use PHPStan\Type\Constant\ConstantIntegerType;
1313
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1414
use PHPStan\Type\Generic\GenericObjectType;
1515
use PHPStan\Type\IntegerType;
16+
use PHPStan\Type\MixedType;
17+
use PHPStan\Type\NullType;
1618
use PHPStan\Type\Type;
1719
use PHPStan\Type\TypeCombinator;
1820
use PHPStan\Type\VoidType;
@@ -25,6 +27,7 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
2527
'execute' => 1,
2628
'executeIgnoreQueryCache' => 1,
2729
'executeUsingQueryCache' => 1,
30+
'getOneOrNullResult' => 0,
2831
'getSingleResult' => 0,
2932
];
3033

@@ -45,69 +48,90 @@ public function getTypeFromMethodCall(
4548
): Type
4649
{
4750
$methodName = $methodReflection->getName();
48-
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName] ?? 0;
4951

50-
$isHydrationModeObject = $this->isHydrationModeObject($methodCall, $scope, $argIndex);
52+
if (!isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
53+
throw new ShouldNotHappenException();
54+
}
55+
56+
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
57+
$args = $methodCall->getArgs();
5158

52-
if (!$isHydrationModeObject) {
53-
return $this->fallbackType($methodReflection);
59+
if (isset($args[$argIndex])) {
60+
$hydrationMode = $scope->getType($args[$argIndex]->value);
61+
} else {
62+
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
63+
$methodReflection->getVariants()
64+
);
65+
$parameter = $parametersAcceptor->getParameters()[$argIndex];
66+
$hydrationMode = $parameter->getDefaultValue() ?? new NullType();
5467
}
5568

5669
$queryType = $scope->getType($methodCall->var);
70+
$queryResultType = $this->getQueryResultType($queryType);
71+
72+
return $this->getMethodReturnTypeForHydrationMode(
73+
$methodReflection,
74+
$hydrationMode,
75+
$queryResultType,
76+
);
77+
}
5778

79+
private function getQueryResultType(Type $queryType): Type
80+
{
5881
if (!$queryType instanceof GenericObjectType) {
59-
return $this->fallbackType($methodReflection);
82+
return new MixedType();
6083
}
6184

6285
$types = $queryType->getTypes();
6386

64-
if (!isset($types[0])) {
65-
return $this->fallbackType($methodReflection);
66-
}
67-
68-
$resultType = $types[0];
87+
return $types[0] ?? new MixedType();
88+
}
6989

70-
if ($resultType instanceof VoidType) {
71-
return $this->fallbackType($methodReflection);
90+
private function getMethodReturnTypeForHydrationMode(
91+
MethodReflection $methodReflection,
92+
Type $hydrationMode,
93+
Type $queryResultType
94+
): Type
95+
{
96+
if ($queryResultType instanceof VoidType) {
97+
// A void query result type indicates an UPDATE or DELETE query.
98+
// In this case all methods return the number of affected rows.
99+
return new IntegerType();
72100
}
73101

74-
if ($methodName === 'getOneOrNullResult') {
75-
return TypeCombinator::addNull($resultType);
102+
if (!$this->isObjectHydrationMode($hydrationMode)) {
103+
// We support only HYDRATE_OBJECT. For other hydration modes, we
104+
// return the declared return type of the method.
105+
return $this->originalReturnType($methodReflection);
76106
}
77107

78-
if ($methodName === 'getSingleResult') {
79-
return $resultType;
108+
switch ($methodReflection->getName()) {
109+
case 'getSingleResult':
110+
return $queryResultType;
111+
case 'getOneOrNullResult':
112+
return TypeCombinator::addNull($queryResultType);
113+
default:
114+
return new ArrayType(
115+
new MixedType(),
116+
$queryResultType
117+
);
80118
}
81-
82-
return new ArrayType(
83-
new IntegerType(),
84-
$resultType
85-
);
86119
}
87120

88-
private function isHydrationModeObject(
89-
MethodCall $methodCall,
90-
Scope $scope,
91-
int $argIndex
92-
): bool
121+
private function isObjectHydrationMode(Type $type): bool
93122
{
94-
$args = $methodCall->getArgs();
95-
96-
if (!isset($args[$argIndex])) {
97-
return true;
98-
}
99-
100-
$argType = $scope->getType($args[$argIndex]->value);
101-
if (!$argType instanceof ConstantIntegerType) {
123+
if (!$type instanceof ConstantIntegerType) {
102124
return false;
103125
}
104126

105-
return $argType->getValue() === Query::HYDRATE_OBJECT;
127+
return $type->getValue() === AbstractQuery::HYDRATE_OBJECT;
106128
}
107129

108-
private function fallbackType(MethodReflection $methodReflection): Type
130+
private function originalReturnType(MethodReflection $methodReflection): Type
109131
{
110-
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
132+
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
133+
$methodReflection->getVariants()
134+
);
111135

112136
return $parametersAcceptor->getReturnType();
113137
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Query;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
class QueryResultDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
8+
{
9+
10+
/** @return iterable<mixed> */
11+
public function dataFileAsserts(): iterable
12+
{
13+
yield from $this->gatherAssertTypes(__DIR__ . '/data/QueryResult/base.php');
14+
}
15+
16+
/**
17+
* @dataProvider dataFileAsserts
18+
* @param string $assertType
19+
* @param string $file
20+
* @param mixed ...$args
21+
*/
22+
public function testFileAsserts(
23+
string $assertType,
24+
string $file,
25+
...$args
26+
): void
27+
{
28+
$this->assertFileAsserts($assertType, $file, ...$args);
29+
}
30+
31+
/** @return string[] */
32+
public static function getAdditionalConfigFiles(): array
33+
{
34+
return [__DIR__ . '/data/QueryResult/config.neon'];
35+
}
36+
37+
}

tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
namespace PHPStan\Type\Doctrine\Query;
44

5-
use Doctrine\ORM\EntityManager;
65
use Doctrine\ORM\EntityManagerInterface;
7-
use Doctrine\ORM\Tools\Setup;
86
use PHPStan\Testing\PHPStanTestCase;
97
use PHPStan\Type\Accessory\AccessoryNumericStringType;
108
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
@@ -42,14 +40,7 @@ public static function getAdditionalConfigFiles(): array
4240

4341
public function setUp(): void
4442
{
45-
$config = Setup::createAnnotationMetadataConfiguration([__DIR__ . '/src'], true, null, null, false);
46-
47-
$conn = [
48-
'driver' => 'pdo_sqlite',
49-
'path' => __DIR__ . '/data/QueryResult/db.sqlite',
50-
];
51-
52-
$this->em = EntityManager::create($conn, $config);
43+
$this->em = require __DIR__ . '/data/QueryResult/entity-manager.php';
5344

5445
$this->descriptorRegistry = self::getContainer()->getByType(DescriptorRegistry::class);
5546
}

0 commit comments

Comments
 (0)