Skip to content

Commit 18ab94d

Browse files
authored
Test hydration modes
1 parent c54ce9b commit 18ab94d

File tree

5 files changed

+713
-138
lines changed

5 files changed

+713
-138
lines changed

extension.neon

Lines changed: 2 additions & 0 deletions
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
-
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
21+
class HydrationModeReturnTypeResolver
22+
{
23+
24+
public function getMethodReturnTypeForHydrationMode(
25+
string $methodName,
26+
int $hydrationMode,
27+
Type $queryKeyType,
28+
Type $queryResultType,
29+
?ObjectManager $objectManager
30+
): ?Type
31+
{
32+
$isVoidType = (new VoidType())->isSuperTypeOf($queryResultType);
33+
34+
if ($isVoidType->yes()) {
35+
// A void query result type indicates an UPDATE or DELETE query.
36+
// In this case all methods return the number of affected rows.
37+
return new IntegerType();
38+
}
39+
40+
if ($isVoidType->maybe()) {
41+
// We can't be sure what the query type is, so we return the
42+
// declared return type of the method.
43+
return null;
44+
}
45+
46+
switch ($hydrationMode) {
47+
case AbstractQuery::HYDRATE_OBJECT:
48+
break;
49+
case AbstractQuery::HYDRATE_ARRAY:
50+
$queryResultType = $this->getArrayHydratedReturnType($queryResultType, $objectManager);
51+
break;
52+
case AbstractQuery::HYDRATE_SIMPLEOBJECT:
53+
$queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType);
54+
break;
55+
default:
56+
return null;
57+
}
58+
59+
if ($queryResultType === null) {
60+
return null;
61+
}
62+
63+
switch ($methodName) {
64+
case 'getSingleResult':
65+
return $queryResultType;
66+
case 'getOneOrNullResult':
67+
$nullableQueryResultType = TypeCombinator::addNull($queryResultType);
68+
if ($queryResultType instanceof BenevolentUnionType) {
69+
$nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType);
70+
}
71+
72+
return $nullableQueryResultType;
73+
case 'toIterable':
74+
return new IterableType(
75+
$queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType,
76+
$queryResultType
77+
);
78+
default:
79+
if ($queryKeyType->isNull()->yes()) {
80+
return AccessoryArrayListType::intersectWith(new ArrayType(
81+
new IntegerType(),
82+
$queryResultType
83+
));
84+
}
85+
return new ArrayType(
86+
$queryKeyType,
87+
$queryResultType
88+
);
89+
}
90+
}
91+
92+
/**
93+
* When we're array-hydrating object, we're not sure of the shape of the array.
94+
* We could return `new ArrayTyp(new MixedType(), new MixedType())`
95+
* but the lack of precision in the array keys/values would give false positive.
96+
*
97+
* @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934
98+
*/
99+
private function getArrayHydratedReturnType(Type $queryResultType, ?ObjectManager $objectManager): ?Type
100+
{
101+
$mixedFound = false;
102+
$queryResultType = TypeTraverser::map(
103+
$queryResultType,
104+
static function (Type $type, callable $traverse) use ($objectManager, &$mixedFound): Type {
105+
$isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type);
106+
if ($isObject->no()) {
107+
return $traverse($type);
108+
}
109+
if (
110+
$isObject->maybe()
111+
|| !$type instanceof TypeWithClassName
112+
|| $objectManager === null
113+
) {
114+
$mixedFound = true;
115+
116+
return new MixedType();
117+
}
118+
119+
/** @var class-string $className */
120+
$className = $type->getClassName();
121+
if (!$objectManager->getMetadataFactory()->hasMetadataFor($className)) {
122+
return $traverse($type);
123+
}
124+
125+
$mixedFound = true;
126+
127+
return new MixedType();
128+
}
129+
);
130+
131+
return $mixedFound ? null : $queryResultType;
132+
}
133+
134+
private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type
135+
{
136+
if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) {
137+
return $queryResultType;
138+
}
139+
140+
return null;
141+
}
142+
143+
}

src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php

Lines changed: 13 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,12 @@
88
use PHPStan\Reflection\MethodReflection;
99
use PHPStan\Reflection\ParametersAcceptorSelector;
1010
use PHPStan\ShouldNotHappenException;
11-
use PHPStan\Type\Accessory\AccessoryArrayListType;
12-
use PHPStan\Type\ArrayType;
13-
use PHPStan\Type\BenevolentUnionType;
1411
use PHPStan\Type\Constant\ConstantIntegerType;
12+
use PHPStan\Type\Doctrine\HydrationModeReturnTypeResolver;
1513
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
1614
use PHPStan\Type\DynamicMethodReturnTypeExtension;
17-
use PHPStan\Type\IntegerType;
18-
use PHPStan\Type\IterableType;
19-
use PHPStan\Type\MixedType;
2015
use PHPStan\Type\NullType;
21-
use PHPStan\Type\ObjectWithoutClassType;
2216
use PHPStan\Type\Type;
23-
use PHPStan\Type\TypeCombinator;
24-
use PHPStan\Type\TypeTraverser;
25-
use PHPStan\Type\TypeUtils;
26-
use PHPStan\Type\TypeWithClassName;
27-
use PHPStan\Type\VoidType;
2817

2918
final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
3019
{
@@ -46,11 +35,16 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
4635
/** @var ObjectMetadataResolver */
4736
private $objectMetadataResolver;
4837

38+
/** @var HydrationModeReturnTypeResolver */
39+
private $hydrationModeReturnTypeResolver;
40+
4941
public function __construct(
50-
ObjectMetadataResolver $objectMetadataResolver
42+
ObjectMetadataResolver $objectMetadataResolver,
43+
HydrationModeReturnTypeResolver $hydrationModeReturnTypeResolver
5144
)
5245
{
5346
$this->objectMetadataResolver = $objectMetadataResolver;
47+
$this->hydrationModeReturnTypeResolver = $hydrationModeReturnTypeResolver;
5448
}
5549

5650
public function getClass(): string
@@ -93,136 +87,17 @@ public function getTypeFromMethodCall(
9387

9488
$queryType = $scope->getType($methodCall->var);
9589

96-
return $this->getMethodReturnTypeForHydrationMode(
97-
$methodReflection,
98-
$hydrationMode,
99-
$queryType->getTemplateType(AbstractQuery::class, 'TKey'),
100-
$queryType->getTemplateType(AbstractQuery::class, 'TResult')
101-
);
102-
}
103-
104-
private function getMethodReturnTypeForHydrationMode(
105-
MethodReflection $methodReflection,
106-
Type $hydrationMode,
107-
Type $queryKeyType,
108-
Type $queryResultType
109-
): ?Type
110-
{
111-
$isVoidType = (new VoidType())->isSuperTypeOf($queryResultType);
112-
113-
if ($isVoidType->yes()) {
114-
// A void query result type indicates an UPDATE or DELETE query.
115-
// In this case all methods return the number of affected rows.
116-
return new IntegerType();
117-
}
118-
119-
if ($isVoidType->maybe()) {
120-
// We can't be sure what the query type is, so we return the
121-
// declared return type of the method.
122-
return null;
123-
}
124-
12590
if (!$hydrationMode instanceof ConstantIntegerType) {
12691
return null;
12792
}
12893

129-
switch ($hydrationMode->getValue()) {
130-
case AbstractQuery::HYDRATE_OBJECT:
131-
break;
132-
case AbstractQuery::HYDRATE_ARRAY:
133-
$queryResultType = $this->getArrayHydratedReturnType($queryResultType);
134-
break;
135-
case AbstractQuery::HYDRATE_SIMPLEOBJECT:
136-
$queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType);
137-
break;
138-
default:
139-
return null;
140-
}
141-
142-
if ($queryResultType === null) {
143-
return null;
144-
}
145-
146-
switch ($methodReflection->getName()) {
147-
case 'getSingleResult':
148-
return $queryResultType;
149-
case 'getOneOrNullResult':
150-
$nullableQueryResultType = TypeCombinator::addNull($queryResultType);
151-
if ($queryResultType instanceof BenevolentUnionType) {
152-
$nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType);
153-
}
154-
155-
return $nullableQueryResultType;
156-
case 'toIterable':
157-
return new IterableType(
158-
$queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType,
159-
$queryResultType
160-
);
161-
default:
162-
if ($queryKeyType->isNull()->yes()) {
163-
return AccessoryArrayListType::intersectWith(new ArrayType(
164-
new IntegerType(),
165-
$queryResultType
166-
));
167-
}
168-
return new ArrayType(
169-
$queryKeyType,
170-
$queryResultType
171-
);
172-
}
173-
}
174-
175-
/**
176-
* When we're array-hydrating object, we're not sure of the shape of the array.
177-
* We could return `new ArrayTyp(new MixedType(), new MixedType())`
178-
* but the lack of precision in the array keys/values would give false positive.
179-
*
180-
* @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934
181-
*/
182-
private function getArrayHydratedReturnType(Type $queryResultType): ?Type
183-
{
184-
$objectManager = $this->objectMetadataResolver->getObjectManager();
185-
186-
$mixedFound = false;
187-
$queryResultType = TypeTraverser::map(
188-
$queryResultType,
189-
static function (Type $type, callable $traverse) use ($objectManager, &$mixedFound): Type {
190-
$isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type);
191-
if ($isObject->no()) {
192-
return $traverse($type);
193-
}
194-
if (
195-
$isObject->maybe()
196-
|| !$type instanceof TypeWithClassName
197-
|| $objectManager === null
198-
) {
199-
$mixedFound = true;
200-
201-
return new MixedType();
202-
}
203-
204-
/** @var class-string $className */
205-
$className = $type->getClassName();
206-
if (!$objectManager->getMetadataFactory()->hasMetadataFor($className)) {
207-
return $traverse($type);
208-
}
209-
210-
$mixedFound = true;
211-
212-
return new MixedType();
213-
}
94+
return $this->hydrationModeReturnTypeResolver->getMethodReturnTypeForHydrationMode(
95+
$methodReflection->getName(),
96+
$hydrationMode->getValue(),
97+
$queryType->getTemplateType(AbstractQuery::class, 'TKey'),
98+
$queryType->getTemplateType(AbstractQuery::class, 'TResult'),
99+
$this->objectMetadataResolver->getObjectManager()
214100
);
215-
216-
return $mixedFound ? null : $queryResultType;
217-
}
218-
219-
private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type
220-
{
221-
if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) {
222-
return $queryResultType;
223-
}
224-
225-
return null;
226101
}
227102

228103
}

0 commit comments

Comments
 (0)