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,54 @@ private function getMethodReturnTypeForHydrationMode(
98
126
return $ this ->originalReturnType ($ methodReflection );
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) {
104
130
return $ this ->originalReturnType ($ methodReflection );
105
131
}
106
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 $ this ->originalReturnType ($ methodReflection );
155
+ }
156
+
107
157
switch ($ methodReflection ->getName ()) {
108
158
case 'getSingleResult ' :
109
159
return $ queryResultType ;
110
160
case 'getOneOrNullResult ' :
111
- return TypeCombinator::addNull ($ queryResultType );
161
+ $ nullableQueryResultType = TypeCombinator::addNull ($ queryResultType );
162
+ if ($ queryResultType instanceof BenevolentUnionType) {
163
+ $ nullableQueryResultType = TypeUtils::toBenevolentUnion ($ nullableQueryResultType );
164
+ }
165
+
166
+ return $ nullableQueryResultType ;
112
167
case 'toIterable ' :
113
168
return new IterableType (
114
169
$ queryKeyType ->isNull ()->yes () ? new IntegerType () : $ queryKeyType ,
115
170
$ queryResultType
116
171
);
117
172
default :
173
+ if ($ singleResult ) {
174
+ return $ queryResultType ;
175
+ }
176
+
118
177
if ($ queryKeyType ->isNull ()->yes ()) {
119
178
return AccessoryArrayListType::intersectWith (new ArrayType (
120
179
new IntegerType (),
@@ -128,13 +187,104 @@ private function getMethodReturnTypeForHydrationMode(
128
187
}
129
188
}
130
189
131
- private function isObjectHydrationMode (Type $ type ): bool
190
+ private function getArrayHydratedReturnType (Type $ queryResultType ): Type
191
+ {
192
+ $ objectManager = $ this ->objectMetadataResolver ->getObjectManager ();
193
+
194
+ return TypeTraverser::map (
195
+ $ queryResultType ,
196
+ static function (Type $ type , callable $ traverse ) use ($ objectManager ): Type {
197
+ $ isObject = (new ObjectWithoutClassType ())->isSuperTypeOf ($ type );
198
+ if ($ isObject ->no ()) {
199
+ return $ traverse ($ type );
200
+ }
201
+ if (
202
+ $ isObject ->maybe ()
203
+ || !$ type instanceof TypeWithClassName
204
+ || $ objectManager === null
205
+ ) {
206
+ return new MixedType ();
207
+ }
208
+
209
+ if (!$ objectManager ->getMetadataFactory ()->hasMetadataFor ($ type ->getClassName ())) {
210
+ return $ traverse ($ type );
211
+ }
212
+
213
+ // We could return `new ArrayTyp(new MixedType(), new MixedType())`
214
+ // but the lack of precision in the array keys/values would give false positive
215
+ // @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934
216
+ return new MixedType ();
217
+ }
218
+ );
219
+ }
220
+
221
+ private function getScalarHydratedReturnType (Type $ queryResultType ): Type
222
+ {
223
+ if (!$ queryResultType ->isArray ()->yes ()) {
224
+ return new ArrayType (new MixedType (), new MixedType ());
225
+ }
226
+
227
+ foreach ($ queryResultType ->getArrays () as $ arrayType ) {
228
+ $ itemType = $ arrayType ->getItemType ();
229
+
230
+ if (
231
+ !(new ObjectWithoutClassType ())->isSuperTypeOf ($ itemType )->no ()
232
+ || !$ itemType ->isArray ()->no ()
233
+ ) {
234
+ return new ArrayType (new MixedType (), new MixedType ());
235
+ }
236
+ }
237
+
238
+ return $ queryResultType ;
239
+ }
240
+
241
+ private function getSimpleObjectHydratedReturnType (Type $ queryResultType ): Type
242
+ {
243
+ if ((new ObjectWithoutClassType ())->isSuperTypeOf ($ queryResultType )->yes ()) {
244
+ return $ queryResultType ;
245
+ }
246
+
247
+ return new MixedType ();
248
+ }
249
+
250
+ private function getSingleScalarHydratedReturnType (Type $ queryResultType ): Type
132
251
{
133
- if (!$ type instanceof ConstantIntegerType) {
134
- return false ;
252
+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
253
+ if (!$ queryResultType ->isConstantArray ()->yes ()) {
254
+ return new MixedType ();
255
+ }
256
+
257
+ $ types = [];
258
+ foreach ($ queryResultType ->getConstantArrays () as $ constantArrayType ) {
259
+ $ values = $ constantArrayType ->getValueTypes ();
260
+ if (count ($ values ) !== 1 ) {
261
+ return new MixedType ();
262
+ }
263
+
264
+ $ types [] = $ constantArrayType ->getFirstIterableValueType ();
265
+ }
266
+
267
+ return TypeCombinator::union (...$ types );
268
+ }
269
+
270
+ private function getScalarColumnHydratedReturnType (Type $ queryResultType ): Type
271
+ {
272
+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
273
+ if (!$ queryResultType ->isConstantArray ()->yes ()) {
274
+ return new MixedType ();
275
+ }
276
+
277
+ $ types = [];
278
+ foreach ($ queryResultType ->getConstantArrays () as $ constantArrayType ) {
279
+ $ values = $ constantArrayType ->getValueTypes ();
280
+ if (count ($ values ) !== 1 ) {
281
+ return new MixedType ();
282
+ }
283
+
284
+ $ types [] = $ constantArrayType ->getFirstIterableValueType ();
135
285
}
136
286
137
- return $ type -> getValue () === AbstractQuery:: HYDRATE_OBJECT ;
287
+ return TypeCombinator:: union (... $ types ) ;
138
288
}
139
289
140
290
private function originalReturnType (MethodReflection $ methodReflection ): Type
0 commit comments