Skip to content

Commit 9d2f535

Browse files
committed
Test hydration modes
1 parent c54ce9b commit 9d2f535

File tree

5 files changed

+506
-126
lines changed

5 files changed

+506
-126
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 PHPStan\Type\Accessory\AccessoryArrayListType;
7+
use PHPStan\Type\ArrayType;
8+
use PHPStan\Type\BenevolentUnionType;
9+
use PHPStan\Type\IntegerType;
10+
use PHPStan\Type\IterableType;
11+
use PHPStan\Type\MixedType;
12+
use PHPStan\Type\ObjectWithoutClassType;
13+
use PHPStan\Type\Type;
14+
use PHPStan\Type\TypeCombinator;
15+
use PHPStan\Type\TypeTraverser;
16+
use PHPStan\Type\TypeUtils;
17+
use PHPStan\Type\TypeWithClassName;
18+
use PHPStan\Type\VoidType;
19+
20+
class HydrationModeReturnTypeResolver
21+
{
22+
23+
public function getMethodReturnTypeForHydrationMode(
24+
string $methodName,
25+
int $hydrationMode,
26+
Type $queryKeyType,
27+
Type $queryResultType
28+
): ?Type
29+
{
30+
$isVoidType = (new VoidType())->isSuperTypeOf($queryResultType);
31+
32+
if ($isVoidType->yes()) {
33+
// A void query result type indicates an UPDATE or DELETE query.
34+
// In this case all methods return the number of affected rows.
35+
return new IntegerType();
36+
}
37+
38+
if ($isVoidType->maybe()) {
39+
// We can't be sure what the query type is, so we return the
40+
// declared return type of the method.
41+
return null;
42+
}
43+
44+
switch ($hydrationMode) {
45+
case AbstractQuery::HYDRATE_OBJECT:
46+
break;
47+
case AbstractQuery::HYDRATE_ARRAY:
48+
$queryResultType = $this->getArrayHydratedReturnType($queryResultType);
49+
break;
50+
case AbstractQuery::HYDRATE_SIMPLEOBJECT:
51+
$queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType);
52+
break;
53+
default:
54+
return null;
55+
}
56+
57+
if ($queryResultType === null) {
58+
return null;
59+
}
60+
61+
switch ($methodName) {
62+
case 'getSingleResult':
63+
return $queryResultType;
64+
case 'getOneOrNullResult':
65+
$nullableQueryResultType = TypeCombinator::addNull($queryResultType);
66+
if ($queryResultType instanceof BenevolentUnionType) {
67+
$nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType);
68+
}
69+
70+
return $nullableQueryResultType;
71+
case 'toIterable':
72+
return new IterableType(
73+
$queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType,
74+
$queryResultType
75+
);
76+
default:
77+
if ($queryKeyType->isNull()->yes()) {
78+
return AccessoryArrayListType::intersectWith(new ArrayType(
79+
new IntegerType(),
80+
$queryResultType
81+
));
82+
}
83+
return new ArrayType(
84+
$queryKeyType,
85+
$queryResultType
86+
);
87+
}
88+
}
89+
90+
/**
91+
* When we're array-hydrating object, we're not sure of the shape of the array.
92+
* We could return `new ArrayTyp(new MixedType(), new MixedType())`
93+
* but the lack of precision in the array keys/values would give false positive.
94+
*
95+
* @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934
96+
*/
97+
private function getArrayHydratedReturnType(Type $queryResultType): ?Type
98+
{
99+
$objectManager = $this->objectMetadataResolver->getObjectManager();
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: 12 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Type\ArrayType;
1313
use PHPStan\Type\BenevolentUnionType;
1414
use PHPStan\Type\Constant\ConstantIntegerType;
15+
use PHPStan\Type\Doctrine\HydrationModeReturnTypeResolver;
1516
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
1617
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1718
use PHPStan\Type\IntegerType;
@@ -46,11 +47,16 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
4647
/** @var ObjectMetadataResolver */
4748
private $objectMetadataResolver;
4849

50+
/** @var HydrationModeReturnTypeResolver */
51+
private $hydrationModeReturnTypeResolver;
52+
4953
public function __construct(
50-
ObjectMetadataResolver $objectMetadataResolver
54+
ObjectMetadataResolver $objectMetadataResolver,
55+
HydrationModeReturnTypeResolver $hydrationModeReturnTypeResolver
5156
)
5257
{
5358
$this->objectMetadataResolver = $objectMetadataResolver;
59+
$this->hydrationModeReturnTypeResolver = $hydrationModeReturnTypeResolver;
5460
}
5561

5662
public function getClass(): string
@@ -93,136 +99,16 @@ public function getTypeFromMethodCall(
9399

94100
$queryType = $scope->getType($methodCall->var);
95101

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-
125102
if (!$hydrationMode instanceof ConstantIntegerType) {
126103
return null;
127104
}
128105

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-
}
106+
return $this->hydrationModeReturnTypeResolver->getMethodReturnTypeForHydrationMode(
107+
$methodReflection->getName(),
108+
$hydrationMode->getValue(),
109+
$queryType->getTemplateType(AbstractQuery::class, 'TKey'),
110+
$queryType->getTemplateType(AbstractQuery::class, 'TResult')
214111
);
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;
226112
}
227113

228114
}

0 commit comments

Comments
 (0)