10
10
use PHPStan \ShouldNotHappenException ;
11
11
use PHPStan \Type \Accessory \AccessoryArrayListType ;
12
12
use PHPStan \Type \ArrayType ;
13
+ use PHPStan \Type \BenevolentUnionType ;
13
14
use PHPStan \Type \Constant \ConstantIntegerType ;
15
+ use PHPStan \Type \Doctrine \ObjectMetadataResolver ;
14
16
use PHPStan \Type \DynamicMethodReturnTypeExtension ;
15
17
use PHPStan \Type \IntegerType ;
16
18
use PHPStan \Type \IterableType ;
19
+ use PHPStan \Type \MixedType ;
17
20
use PHPStan \Type \NullType ;
21
+ use PHPStan \Type \ObjectWithoutClassType ;
18
22
use PHPStan \Type \Type ;
19
23
use PHPStan \Type \TypeCombinator ;
24
+ use PHPStan \Type \TypeTraverser ;
25
+ use PHPStan \Type \TypeUtils ;
26
+ use PHPStan \Type \TypeWithClassName ;
20
27
use PHPStan \Type \VoidType ;
28
+ use function count ;
21
29
22
30
final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
23
31
{
@@ -32,14 +40,32 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
32
40
'getSingleResult ' => 0 ,
33
41
];
34
42
43
+ private const METHOD_HYDRATION_MODE = [
44
+ 'getArrayResult ' => AbstractQuery::HYDRATE_ARRAY ,
45
+ 'getScalarResult ' => AbstractQuery::HYDRATE_SCALAR ,
46
+ 'getSingleColumnResult ' => AbstractQuery::HYDRATE_SCALAR_COLUMN ,
47
+ 'getSingleScalarResult ' => AbstractQuery::HYDRATE_SINGLE_SCALAR ,
48
+ ];
49
+
50
+ /** @var ObjectMetadataResolver */
51
+ private $ objectMetadataResolver ;
52
+
53
+ public function __construct (
54
+ ObjectMetadataResolver $ objectMetadataResolver
55
+ )
56
+ {
57
+ $ this ->objectMetadataResolver = $ objectMetadataResolver ;
58
+ }
59
+
35
60
public function getClass (): string
36
61
{
37
62
return AbstractQuery::class;
38
63
}
39
64
40
65
public function isMethodSupported (MethodReflection $ methodReflection ): bool
41
66
{
42
- return isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodReflection ->getName ()]);
67
+ return isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodReflection ->getName ()])
68
+ || isset (self ::METHOD_HYDRATION_MODE [$ methodReflection ->getName ()]);
43
69
}
44
70
45
71
public function getTypeFromMethodCall (
@@ -50,21 +76,23 @@ public function getTypeFromMethodCall(
50
76
{
51
77
$ methodName = $ methodReflection ->getName ();
52
78
53
- if (!isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodName ])) {
54
- throw new ShouldNotHappenException ();
55
- }
56
-
57
- $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ];
58
- $ args = $ methodCall ->getArgs ();
79
+ if (isset (self ::METHOD_HYDRATION_MODE [$ methodName ])) {
80
+ $ hydrationMode = new ConstantIntegerType (self ::METHOD_HYDRATION_MODE [$ methodName ]);
81
+ } elseif (isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodName ])) {
82
+ $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ];
83
+ $ args = $ methodCall ->getArgs ();
59
84
60
- if (isset ($ args [$ argIndex ])) {
61
- $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
85
+ if (isset ($ args [$ argIndex ])) {
86
+ $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
87
+ } else {
88
+ $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
89
+ $ methodReflection ->getVariants ()
90
+ );
91
+ $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
92
+ $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
93
+ }
62
94
} else {
63
- $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
64
- $ methodReflection ->getVariants ()
65
- );
66
- $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
67
- $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
95
+ throw new ShouldNotHappenException ();
68
96
}
69
97
70
98
$ queryType = $ scope ->getType ($ methodCall ->var );
@@ -98,23 +126,58 @@ private function getMethodReturnTypeForHydrationMode(
98
126
return null ;
99
127
}
100
128
101
- if (!$ this ->isObjectHydrationMode ($ hydrationMode )) {
102
- // We support only HYDRATE_OBJECT. For other hydration modes, we
103
- // return the declared return type of the method.
129
+ if (!$ hydrationMode instanceof ConstantIntegerType) {
130
+ return null ;
131
+ }
132
+
133
+ $ singleResult = false ;
134
+ switch ($ hydrationMode ->getValue ()) {
135
+ case AbstractQuery::HYDRATE_OBJECT :
136
+ break ;
137
+ case AbstractQuery::HYDRATE_ARRAY :
138
+ $ queryResultType = $ this ->getArrayHydratedReturnType ($ queryResultType );
139
+ break ;
140
+ case AbstractQuery::HYDRATE_SCALAR :
141
+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
142
+ break ;
143
+ case AbstractQuery::HYDRATE_SINGLE_SCALAR :
144
+ $ singleResult = true ;
145
+ $ queryResultType = $ this ->getSingleScalarHydratedReturnType ($ queryResultType );
146
+ break ;
147
+ case AbstractQuery::HYDRATE_SIMPLEOBJECT :
148
+ $ queryResultType = $ this ->getSimpleObjectHydratedReturnType ($ queryResultType );
149
+ break ;
150
+ case AbstractQuery::HYDRATE_SCALAR_COLUMN :
151
+ $ queryResultType = $ this ->getScalarColumnHydratedReturnType ($ queryResultType );
152
+ break ;
153
+ default :
154
+ return null ;
155
+ }
156
+
157
+ if ($ queryResultType === null ) {
104
158
return null ;
105
159
}
106
160
107
161
switch ($ methodReflection ->getName ()) {
108
162
case 'getSingleResult ' :
109
163
return $ queryResultType ;
110
164
case 'getOneOrNullResult ' :
111
- return TypeCombinator::addNull ($ queryResultType );
165
+ $ nullableQueryResultType = TypeCombinator::addNull ($ queryResultType );
166
+ if ($ queryResultType instanceof BenevolentUnionType) {
167
+ $ nullableQueryResultType = TypeUtils::toBenevolentUnion ($ nullableQueryResultType );
168
+ }
169
+
170
+ return $ nullableQueryResultType ;
112
171
case 'toIterable ' :
113
172
return new IterableType (
114
173
$ queryKeyType ->isNull ()->yes () ? new IntegerType () : $ queryKeyType ,
115
174
$ queryResultType
116
175
);
117
176
default :
177
+ if ($ singleResult ) {
178
+ return $ queryResultType ;
179
+ }
180
+
118
181
if ($ queryKeyType ->isNull ()->yes ()) {
119
182
return AccessoryArrayListType::intersectWith (new ArrayType (
120
183
new IntegerType (),
@@ -128,13 +191,127 @@ private function getMethodReturnTypeForHydrationMode(
128
191
}
129
192
}
130
193
131
- private function isObjectHydrationMode (Type $ type ): bool
194
+ /**
195
+ * When we're array-hydrating object, we're not sure of the shape of the array.
196
+ * We could return `new ArrayTyp(new MixedType(), new MixedType())`
197
+ * but the lack of precision in the array keys/values would give false positive.
198
+ *
199
+ * @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934
200
+ */
201
+ private function getArrayHydratedReturnType (Type $ queryResultType ): ?Type
132
202
{
133
- if (!$ type instanceof ConstantIntegerType) {
134
- return false ;
203
+ $ objectManager = $ this ->objectMetadataResolver ->getObjectManager ();
204
+
205
+ $ mixedFound = false ;
206
+ $ queryResultType = TypeTraverser::map (
207
+ $ queryResultType ,
208
+ static function (Type $ type , callable $ traverse ) use ($ objectManager , &$ mixedFound ): Type {
209
+ $ isObject = (new ObjectWithoutClassType ())->isSuperTypeOf ($ type );
210
+ if ($ isObject ->no ()) {
211
+ return $ traverse ($ type );
212
+ }
213
+ if (
214
+ $ isObject ->maybe ()
215
+ || !$ type instanceof TypeWithClassName
216
+ || $ objectManager === null
217
+ ) {
218
+ $ mixedFound = true ;
219
+
220
+ return new MixedType ();
221
+ }
222
+
223
+ /** @var class-string $className */
224
+ $ className = $ type ->getClassName ();
225
+ if (!$ objectManager ->getMetadataFactory ()->hasMetadataFor ($ className )) {
226
+ return $ traverse ($ type );
227
+ }
228
+
229
+ $ mixedFound = true ;
230
+
231
+ return new MixedType ();
232
+ }
233
+ );
234
+
235
+ return $ mixedFound ? null : $ queryResultType ;
236
+ }
237
+
238
+ /**
239
+ * When we're scalar-hydrating object, we're not sure of the shape of the array.
240
+ * We could return `new ArrayTyp(new MixedType(), new MixedType())`
241
+ * but the lack of precision in the array keys/values would give false positive.
242
+ *
243
+ * @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544
244
+ */
245
+ private function getScalarHydratedReturnType (Type $ queryResultType ): ?Type
246
+ {
247
+ if (!$ queryResultType ->isArray ()->yes ()) {
248
+ return null ;
249
+ }
250
+
251
+ foreach ($ queryResultType ->getArrays () as $ arrayType ) {
252
+ $ itemType = $ arrayType ->getItemType ();
253
+
254
+ if (
255
+ !(new ObjectWithoutClassType ())->isSuperTypeOf ($ itemType )->no ()
256
+ || !$ itemType ->isArray ()->no ()
257
+ ) {
258
+ // We could return `new ArrayTyp(new MixedType(), new MixedType())`
259
+ // but the lack of precision in the array keys/values would give false positive
260
+ // @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544
261
+ return null ;
262
+ }
263
+ }
264
+
265
+ return $ queryResultType ;
266
+ }
267
+
268
+ private function getSimpleObjectHydratedReturnType (Type $ queryResultType ): ?Type
269
+ {
270
+ if ((new ObjectWithoutClassType ())->isSuperTypeOf ($ queryResultType )->yes ()) {
271
+ return $ queryResultType ;
272
+ }
273
+
274
+ return null ;
275
+ }
276
+
277
+ private function getSingleScalarHydratedReturnType (Type $ queryResultType ): ?Type
278
+ {
279
+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
280
+ if ($ queryResultType === null || !$ queryResultType ->isConstantArray ()->yes ()) {
281
+ return null ;
282
+ }
283
+
284
+ $ types = [];
285
+ foreach ($ queryResultType ->getConstantArrays () as $ constantArrayType ) {
286
+ $ values = $ constantArrayType ->getValueTypes ();
287
+ if (count ($ values ) !== 1 ) {
288
+ return null ;
289
+ }
290
+
291
+ $ types [] = $ constantArrayType ->getFirstIterableValueType ();
292
+ }
293
+
294
+ return TypeCombinator::union (...$ types );
295
+ }
296
+
297
+ private function getScalarColumnHydratedReturnType (Type $ queryResultType ): ?Type
298
+ {
299
+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
300
+ if ($ queryResultType === null || !$ queryResultType ->isConstantArray ()->yes ()) {
301
+ return null ;
302
+ }
303
+
304
+ $ types = [];
305
+ foreach ($ queryResultType ->getConstantArrays () as $ constantArrayType ) {
306
+ $ values = $ constantArrayType ->getValueTypes ();
307
+ if (count ($ values ) !== 1 ) {
308
+ return null ;
309
+ }
310
+
311
+ $ types [] = $ constantArrayType ->getFirstIterableValueType ();
135
312
}
136
313
137
- return $ type -> getValue () === AbstractQuery:: HYDRATE_OBJECT ;
314
+ return TypeCombinator:: union (... $ types ) ;
138
315
}
139
316
140
317
}
0 commit comments