|
12 | 12 | use PHPStan\Type\ArrayType; |
13 | 13 | use PHPStan\Type\BenevolentUnionType; |
14 | 14 | use PHPStan\Type\Constant\ConstantIntegerType; |
| 15 | +use PHPStan\Type\Doctrine\HydrationModeReturnTypeResolver; |
15 | 16 | use PHPStan\Type\Doctrine\ObjectMetadataResolver; |
16 | 17 | use PHPStan\Type\DynamicMethodReturnTypeExtension; |
17 | 18 | use PHPStan\Type\IntegerType; |
@@ -46,11 +47,16 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn |
46 | 47 | /** @var ObjectMetadataResolver */ |
47 | 48 | private $objectMetadataResolver; |
48 | 49 |
|
| 50 | + /** @var HydrationModeReturnTypeResolver */ |
| 51 | + private $hydrationModeReturnTypeResolver; |
| 52 | + |
49 | 53 | public function __construct( |
50 | | - ObjectMetadataResolver $objectMetadataResolver |
| 54 | + ObjectMetadataResolver $objectMetadataResolver, |
| 55 | + HydrationModeReturnTypeResolver $hydrationModeReturnTypeResolver |
51 | 56 | ) |
52 | 57 | { |
53 | 58 | $this->objectMetadataResolver = $objectMetadataResolver; |
| 59 | + $this->hydrationModeReturnTypeResolver = $hydrationModeReturnTypeResolver; |
54 | 60 | } |
55 | 61 |
|
56 | 62 | public function getClass(): string |
@@ -93,136 +99,16 @@ public function getTypeFromMethodCall( |
93 | 99 |
|
94 | 100 | $queryType = $scope->getType($methodCall->var); |
95 | 101 |
|
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 | | - |
125 | 102 | if (!$hydrationMode instanceof ConstantIntegerType) { |
126 | 103 | return null; |
127 | 104 | } |
128 | 105 |
|
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') |
214 | 111 | ); |
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; |
226 | 112 | } |
227 | 113 |
|
228 | 114 | } |
0 commit comments