10
10
use PHPStan \ShouldNotHappenException ;
11
11
use PHPStan \Type \Accessory \AccessoryArrayListType ;
12
12
use PHPStan \Type \ArrayType ;
13
+ use PHPStan \Type \Constant \ConstantArrayType ;
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 \TypeWithClassName ;
20
26
use PHPStan \Type \VoidType ;
27
+ use function count ;
21
28
22
29
final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
23
30
{
@@ -32,14 +39,32 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
32
39
'getSingleResult ' => 0 ,
33
40
];
34
41
42
+ private const METHOD_HYDRATION_MODE = [
43
+ 'getArrayResult ' => AbstractQuery::HYDRATE_ARRAY ,
44
+ 'getScalarResult ' => AbstractQuery::HYDRATE_SCALAR ,
45
+ 'getSingleColumnResult ' => AbstractQuery::HYDRATE_SCALAR_COLUMN ,
46
+ 'getSingleScalarResult ' => AbstractQuery::HYDRATE_SINGLE_SCALAR ,
47
+ ];
48
+
49
+ /** @var ObjectMetadataResolver */
50
+ private $ objectMetadataResolver ;
51
+
52
+ public function __construct (
53
+ ObjectMetadataResolver $ objectMetadataResolver
54
+ )
55
+ {
56
+ $ this ->objectMetadataResolver = $ objectMetadataResolver ;
57
+ }
58
+
35
59
public function getClass (): string
36
60
{
37
61
return AbstractQuery::class;
38
62
}
39
63
40
64
public function isMethodSupported (MethodReflection $ methodReflection ): bool
41
65
{
42
- return isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodReflection ->getName ()]);
66
+ return isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodReflection ->getName ()])
67
+ || isset (self ::METHOD_HYDRATION_MODE [$ methodReflection ->getName ()]);
43
68
}
44
69
45
70
public function getTypeFromMethodCall (
@@ -50,21 +75,23 @@ public function getTypeFromMethodCall(
50
75
{
51
76
$ methodName = $ methodReflection ->getName ();
52
77
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 ();
78
+ if (isset (self ::METHOD_HYDRATION_MODE [$ methodName ])) {
79
+ $ hydrationMode = new ConstantIntegerType (self ::METHOD_HYDRATION_MODE [$ methodName ]);
80
+ } elseif (isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodName ])) {
81
+ $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ];
82
+ $ args = $ methodCall ->getArgs ();
59
83
60
- if (isset ($ args [$ argIndex ])) {
61
- $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
84
+ if (isset ($ args [$ argIndex ])) {
85
+ $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
86
+ } else {
87
+ $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
88
+ $ methodReflection ->getVariants ()
89
+ );
90
+ $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
91
+ $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
92
+ }
62
93
} else {
63
- $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
64
- $ methodReflection ->getVariants ()
65
- );
66
- $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
67
- $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
94
+ throw new ShouldNotHappenException ();
68
95
}
69
96
70
97
$ queryType = $ scope ->getType ($ methodCall ->var );
@@ -98,12 +125,34 @@ private function getMethodReturnTypeForHydrationMode(
98
125
return $ this ->originalReturnType ($ methodReflection );
99
126
}
100
127
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.
128
+ if (!$ hydrationMode instanceof ConstantIntegerType) {
104
129
return $ this ->originalReturnType ($ methodReflection );
105
130
}
106
131
132
+ $ singleResult = false ;
133
+ switch ($ hydrationMode ->getValue ()) {
134
+ case AbstractQuery::HYDRATE_OBJECT :
135
+ break ;
136
+ case AbstractQuery::HYDRATE_ARRAY :
137
+ $ queryResultType = $ this ->getArrayHydratedReturnType ($ queryResultType );
138
+ break ;
139
+ case AbstractQuery::HYDRATE_SCALAR :
140
+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
141
+ break ;
142
+ case AbstractQuery::HYDRATE_SINGLE_SCALAR :
143
+ $ singleResult = true ;
144
+ $ queryResultType = $ this ->getSingleScalarHydratedReturnType ($ queryResultType );
145
+ break ;
146
+ case AbstractQuery::HYDRATE_SIMPLEOBJECT :
147
+ $ queryResultType = $ this ->getSimpleObjectHydratedReturnType ($ queryResultType );
148
+ break ;
149
+ case AbstractQuery::HYDRATE_SCALAR_COLUMN :
150
+ $ queryResultType = $ this ->getScalarColumnHydratedReturnType ($ queryResultType );
151
+ break ;
152
+ default :
153
+ return $ this ->originalReturnType ($ methodReflection );
154
+ }
155
+
107
156
switch ($ methodReflection ->getName ()) {
108
157
case 'getSingleResult ' :
109
158
return $ queryResultType ;
@@ -115,6 +164,10 @@ private function getMethodReturnTypeForHydrationMode(
115
164
$ queryResultType
116
165
);
117
166
default :
167
+ if ($ singleResult ) {
168
+ return $ queryResultType ;
169
+ }
170
+
118
171
if ($ queryKeyType ->isNull ()->yes ()) {
119
172
return AccessoryArrayListType::intersectWith (new ArrayType (
120
173
new IntegerType (),
@@ -128,13 +181,86 @@ private function getMethodReturnTypeForHydrationMode(
128
181
}
129
182
}
130
183
131
- private function isObjectHydrationMode (Type $ type ): bool
184
+ private function getArrayHydratedReturnType (Type $ queryResultType ): Type
185
+ {
186
+ $ objectManager = $ this ->objectMetadataResolver ->getObjectManager ();
187
+
188
+ return TypeTraverser::map (
189
+ $ queryResultType ,
190
+ static function (Type $ type , callable $ traverse ) use ($ objectManager ): Type {
191
+ $ isObject = (new ObjectWithoutClassType ())->isSuperTypeOf ($ type );
192
+ if ($ isObject ->no ()) {
193
+ return $ traverse ($ type );
194
+ }
195
+ if (
196
+ $ isObject ->maybe ()
197
+ || !$ type instanceof TypeWithClassName
198
+ || $ objectManager === null
199
+ ) {
200
+ return new MixedType ();
201
+ }
202
+
203
+ return $ objectManager ->getMetadataFactory ()->hasMetadataFor ($ type ->getClassName ())
204
+ ? new ArrayType (new MixedType (), new MixedType ())
205
+ : $ traverse ($ type );
206
+ }
207
+ );
208
+ }
209
+
210
+ private function getScalarHydratedReturnType (Type $ queryResultType ): Type
211
+ {
212
+ if (!$ queryResultType instanceof ArrayType) {
213
+ return new ArrayType (new MixedType (), new MixedType ());
214
+ }
215
+
216
+ $ itemType = $ queryResultType ->getItemType ();
217
+ $ hasNoObject = (new ObjectWithoutClassType ())->isSuperTypeOf ($ itemType )->no ();
218
+ $ hasNoArray = $ itemType ->isArray ()->no ();
219
+
220
+ if ($ hasNoArray && $ hasNoObject ) {
221
+ return $ queryResultType ;
222
+ }
223
+
224
+ return new ArrayType (new MixedType (), new MixedType ());
225
+ }
226
+
227
+ private function getSimpleObjectHydratedReturnType (Type $ queryResultType ): Type
132
228
{
133
- if (!$ type instanceof ConstantIntegerType) {
134
- return false ;
229
+ if ((new ObjectWithoutClassType ())->isSuperTypeOf ($ queryResultType )->yes ()) {
230
+ return $ queryResultType ;
231
+ }
232
+
233
+ return new MixedType ();
234
+ }
235
+
236
+ private function getSingleScalarHydratedReturnType (Type $ queryResultType ): Type
237
+ {
238
+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
239
+ if (!$ queryResultType instanceof ConstantArrayType) {
240
+ return new MixedType ();
241
+ }
242
+
243
+ $ values = $ queryResultType ->getValueTypes ();
244
+ if (count ($ values ) !== 1 ) {
245
+ return new MixedType ();
246
+ }
247
+
248
+ return $ queryResultType ->getFirstIterableValueType ();
249
+ }
250
+
251
+ private function getScalarColumnHydratedReturnType (Type $ queryResultType ): Type
252
+ {
253
+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
254
+ if (!$ queryResultType instanceof ConstantArrayType) {
255
+ return new MixedType ();
256
+ }
257
+
258
+ $ values = $ queryResultType ->getValueTypes ();
259
+ if (count ($ values ) !== 1 ) {
260
+ return new MixedType ();
135
261
}
136
262
137
- return $ type -> getValue () === AbstractQuery:: HYDRATE_OBJECT ;
263
+ return $ queryResultType -> getFirstIterableValueType () ;
138
264
}
139
265
140
266
private function originalReturnType (MethodReflection $ methodReflection ): Type
0 commit comments