Skip to content

Commit 577d95a

Browse files
committed
Test hydration modes
1 parent 7bdb3b2 commit 577d95a

File tree

5 files changed

+593
-224
lines changed

5 files changed

+593
-224
lines changed

Diff for: extension.neon

+2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ services:
9191

9292
-
9393
class: PHPStan\Doctrine\Driver\DriverDetector
94+
-
95+
class: PHPStan\Type\Doctrine\HydrationModeReturnTypeResolver
9496
-
9597
class: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension
9698
-
+229
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine;
4+
5+
use Doctrine\ORM\AbstractQuery;
6+
use Doctrine\Persistence\ObjectManager;
7+
use PHPStan\Type\Accessory\AccessoryArrayListType;
8+
use PHPStan\Type\ArrayType;
9+
use PHPStan\Type\BenevolentUnionType;
10+
use PHPStan\Type\IntegerType;
11+
use PHPStan\Type\IterableType;
12+
use PHPStan\Type\MixedType;
13+
use PHPStan\Type\ObjectWithoutClassType;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeCombinator;
16+
use PHPStan\Type\TypeTraverser;
17+
use PHPStan\Type\TypeUtils;
18+
use PHPStan\Type\TypeWithClassName;
19+
use PHPStan\Type\VoidType;
20+
use function count;
21+
22+
class HydrationModeReturnTypeResolver
23+
{
24+
25+
public function getMethodReturnTypeForHydrationMode(
26+
string $methodName,
27+
int $hydrationMode,
28+
Type $queryKeyType,
29+
Type $queryResultType,
30+
?ObjectManager $objectManager
31+
): ?Type
32+
{
33+
$isVoidType = (new VoidType())->isSuperTypeOf($queryResultType);
34+
35+
if ($isVoidType->yes()) {
36+
// A void query result type indicates an UPDATE or DELETE query.
37+
// In this case all methods return the number of affected rows.
38+
return new IntegerType();
39+
}
40+
41+
if ($isVoidType->maybe()) {
42+
// We can't be sure what the query type is, so we return the
43+
// declared return type of the method.
44+
return null;
45+
}
46+
47+
$singleResult = false;
48+
switch ($hydrationMode) {
49+
case AbstractQuery::HYDRATE_OBJECT:
50+
break;
51+
case AbstractQuery::HYDRATE_ARRAY:
52+
$queryResultType = $this->getArrayHydratedReturnType($queryResultType, $objectManager);
53+
break;
54+
case AbstractQuery::HYDRATE_SCALAR:
55+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
56+
break;
57+
case AbstractQuery::HYDRATE_SINGLE_SCALAR:
58+
$singleResult = true;
59+
$queryResultType = $this->getSingleScalarHydratedReturnType($queryResultType);
60+
break;
61+
case AbstractQuery::HYDRATE_SIMPLEOBJECT:
62+
$queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType);
63+
break;
64+
case AbstractQuery::HYDRATE_SCALAR_COLUMN:
65+
$queryResultType = $this->getScalarColumnHydratedReturnType($queryResultType);
66+
break;
67+
default:
68+
return null;
69+
}
70+
71+
if ($queryResultType === null) {
72+
return null;
73+
}
74+
75+
switch ($methodName) {
76+
case 'getSingleResult':
77+
return $queryResultType;
78+
case 'getOneOrNullResult':
79+
$nullableQueryResultType = TypeCombinator::addNull($queryResultType);
80+
if ($queryResultType instanceof BenevolentUnionType) {
81+
$nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType);
82+
}
83+
84+
return $nullableQueryResultType;
85+
case 'toIterable':
86+
return new IterableType(
87+
$queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType,
88+
$queryResultType
89+
);
90+
default:
91+
if ($singleResult) {
92+
return $queryResultType;
93+
}
94+
95+
if ($queryKeyType->isNull()->yes()) {
96+
return AccessoryArrayListType::intersectWith(new ArrayType(
97+
new IntegerType(),
98+
$queryResultType
99+
));
100+
}
101+
return new ArrayType(
102+
$queryKeyType,
103+
$queryResultType
104+
);
105+
}
106+
}
107+
108+
/**
109+
* When we're array-hydrating object, we're not sure of the shape of the array.
110+
* We could return `new ArrayTyp(new MixedType(), new MixedType())`
111+
* but the lack of precision in the array keys/values would give false positive.
112+
*
113+
* @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934
114+
*/
115+
private function getArrayHydratedReturnType(Type $queryResultType, ?ObjectManager $objectManager): ?Type
116+
{
117+
$mixedFound = false;
118+
$queryResultType = TypeTraverser::map(
119+
$queryResultType,
120+
static function (Type $type, callable $traverse) use ($objectManager, &$mixedFound): Type {
121+
$isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type);
122+
if ($isObject->no()) {
123+
return $traverse($type);
124+
}
125+
if (
126+
$isObject->maybe()
127+
|| !$type instanceof TypeWithClassName
128+
|| $objectManager === null
129+
) {
130+
$mixedFound = true;
131+
132+
return new MixedType();
133+
}
134+
135+
/** @var class-string $className */
136+
$className = $type->getClassName();
137+
if (!$objectManager->getMetadataFactory()->hasMetadataFor($className)) {
138+
return $traverse($type);
139+
}
140+
141+
$mixedFound = true;
142+
143+
return new MixedType();
144+
}
145+
);
146+
147+
return $mixedFound ? null : $queryResultType;
148+
}
149+
150+
/**
151+
* When we're scalar-hydrating object, we're not sure of the shape of the array.
152+
* We could return `new ArrayTyp(new MixedType(), new MixedType())`
153+
* but the lack of precision in the array keys/values would give false positive.
154+
*
155+
* @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544
156+
*/
157+
private function getScalarHydratedReturnType(Type $queryResultType): ?Type
158+
{
159+
if (!$queryResultType->isArray()->yes()) {
160+
return null;
161+
}
162+
163+
foreach ($queryResultType->getArrays() as $arrayType) {
164+
$itemType = $arrayType->getItemType();
165+
166+
if (
167+
!(new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no()
168+
|| !$itemType->isArray()->no()
169+
) {
170+
// We could return `new ArrayTyp(new MixedType(), new MixedType())`
171+
// but the lack of precision in the array keys/values would give false positive
172+
// @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544
173+
return null;
174+
}
175+
}
176+
177+
return $queryResultType;
178+
}
179+
180+
private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type
181+
{
182+
if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) {
183+
return $queryResultType;
184+
}
185+
186+
return null;
187+
}
188+
189+
private function getSingleScalarHydratedReturnType(Type $queryResultType): ?Type
190+
{
191+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
192+
if ($queryResultType === null || !$queryResultType->isConstantArray()->yes()) {
193+
return null;
194+
}
195+
196+
$types = [];
197+
foreach ($queryResultType->getConstantArrays() as $constantArrayType) {
198+
$values = $constantArrayType->getValueTypes();
199+
if (count($values) !== 1) {
200+
return null;
201+
}
202+
203+
$types[] = $constantArrayType->getFirstIterableValueType();
204+
}
205+
206+
return TypeCombinator::union(...$types);
207+
}
208+
209+
private function getScalarColumnHydratedReturnType(Type $queryResultType): ?Type
210+
{
211+
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
212+
if ($queryResultType === null || !$queryResultType->isConstantArray()->yes()) {
213+
return null;
214+
}
215+
216+
$types = [];
217+
foreach ($queryResultType->getConstantArrays() as $constantArrayType) {
218+
$values = $constantArrayType->getValueTypes();
219+
if (count($values) !== 1) {
220+
return null;
221+
}
222+
223+
$types[] = $constantArrayType->getFirstIterableValueType();
224+
}
225+
226+
return TypeCombinator::union(...$types);
227+
}
228+
229+
}

0 commit comments

Comments
 (0)