1010use PHPStan \ShouldNotHappenException ;
1111use PHPStan \Type \Accessory \AccessoryArrayListType ;
1212use PHPStan \Type \ArrayType ;
13+ use PHPStan \Type \Constant \ConstantArrayType ;
1314use PHPStan \Type \Constant \ConstantIntegerType ;
1415use PHPStan \Type \DynamicMethodReturnTypeExtension ;
1516use PHPStan \Type \GenericTypeVariableResolver ;
1617use PHPStan \Type \IntegerType ;
1718use PHPStan \Type \IterableType ;
1819use PHPStan \Type \MixedType ;
1920use PHPStan \Type \NullType ;
21+ use PHPStan \Type \ObjectWithoutClassType ;
2022use PHPStan \Type \Type ;
2123use PHPStan \Type \TypeCombinator ;
24+ use PHPStan \Type \TypeTraverser ;
2225use PHPStan \Type \TypeWithClassName ;
2326use PHPStan \Type \VoidType ;
27+ use function count ;
2428
2529final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
2630{
@@ -35,14 +39,22 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
3539 'getSingleResult ' => 0 ,
3640 ];
3741
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+
3849 public function getClass (): string
3950 {
4051 return AbstractQuery::class;
4152 }
4253
4354 public function isMethodSupported (MethodReflection $ methodReflection ): bool
4455 {
45- return isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodReflection ->getName ()]);
56+ return isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodReflection ->getName ()])
57+ || isset (self ::METHOD_HYDRATION_MODE [$ methodReflection ->getName ()]);
4658 }
4759
4860 public function getTypeFromMethodCall (
@@ -53,21 +65,23 @@ public function getTypeFromMethodCall(
5365 {
5466 $ methodName = $ methodReflection ->getName ();
5567
56- if (!isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodName ])) {
57- throw new ShouldNotHappenException ();
58- }
59-
60- $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ];
61- $ args = $ methodCall ->getArgs ();
68+ if (isset (self ::METHOD_HYDRATION_MODE [$ methodName ])) {
69+ $ hydrationMode = new ConstantIntegerType (self ::METHOD_HYDRATION_MODE [$ methodName ]);
70+ } elseif (isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodName ])) {
71+ $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ];
72+ $ args = $ methodCall ->getArgs ();
6273
63- if (isset ($ args [$ argIndex ])) {
64- $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
74+ if (isset ($ args [$ argIndex ])) {
75+ $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
76+ } else {
77+ $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
78+ $ methodReflection ->getVariants ()
79+ );
80+ $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
81+ $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
82+ }
6583 } else {
66- $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
67- $ methodReflection ->getVariants ()
68- );
69- $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
70- $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
84+ throw new ShouldNotHappenException ();
7185 }
7286
7387 $ queryType = $ scope ->getType ($ methodCall ->var );
@@ -131,12 +145,32 @@ private function getMethodReturnTypeForHydrationMode(
131145 return $ this ->originalReturnType ($ methodReflection );
132146 }
133147
134- if (!$ this ->isObjectHydrationMode ($ hydrationMode )) {
135- // We support only HYDRATE_OBJECT. For other hydration modes, we
136- // return the declared return type of the method.
148+ if (!$ hydrationMode instanceof ConstantIntegerType) {
137149 return $ this ->originalReturnType ($ methodReflection );
138150 }
139151
152+ switch ($ hydrationMode ->getValue ()) {
153+ case AbstractQuery::HYDRATE_OBJECT :
154+ break ;
155+ case AbstractQuery::HYDRATE_ARRAY :
156+ $ queryResultType = $ this ->getArrayHydratedReturnType ($ queryResultType );
157+ break ;
158+ case AbstractQuery::HYDRATE_SCALAR :
159+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
160+ break ;
161+ case AbstractQuery::HYDRATE_SINGLE_SCALAR :
162+ $ queryResultType = $ this ->getSingleScalarHydratedReturnType ($ queryResultType );
163+ break ;
164+ case AbstractQuery::HYDRATE_SIMPLEOBJECT :
165+ $ queryResultType = $ this ->getSimpleObjectHydratedReturnType ($ queryResultType );
166+ break ;
167+ case AbstractQuery::HYDRATE_SCALAR_COLUMN :
168+ $ queryResultType = $ this ->getScalarColumnHydratedReturnType ($ queryResultType );
169+ break ;
170+ default :
171+ return $ this ->originalReturnType ($ methodReflection );
172+ }
173+
140174 switch ($ methodReflection ->getName ()) {
141175 case 'getSingleResult ' :
142176 return $ queryResultType ;
@@ -161,13 +195,78 @@ private function getMethodReturnTypeForHydrationMode(
161195 }
162196 }
163197
164- private function isObjectHydrationMode (Type $ type ): bool
198+ private function getArrayHydratedReturnType (Type $ queryResultType ): Type
165199 {
166- if (!$ type instanceof ConstantIntegerType) {
167- return false ;
200+ return TypeTraverser::map (
201+ $ queryResultType ,
202+ static function (Type $ type , callable $ traverse ): Type {
203+ $ isObject = (new ObjectWithoutClassType ())->isSuperTypeOf ($ type );
204+ if ($ isObject ->yes ()) {
205+ return new ArrayType (new MixedType (), new MixedType ());
206+ }
207+ if ($ isObject ->maybe ()) {
208+ return new MixedType ();
209+ }
210+
211+ return $ traverse ($ type );
212+ }
213+ );
214+ }
215+
216+ private function getScalarHydratedReturnType (Type $ queryResultType ): Type
217+ {
218+ if (!$ queryResultType instanceof ArrayType) {
219+ return new ArrayType (new MixedType (), new MixedType ());
220+ }
221+
222+ $ itemType = $ queryResultType ->getItemType ();
223+ $ hasNoObject = (new ObjectWithoutClassType ())->isSuperTypeOf ($ itemType )->no ();
224+ $ hasNoArray = $ itemType ->isArray ()->no ();
225+
226+ if ($ hasNoArray && $ hasNoObject ) {
227+ return $ queryResultType ;
228+ }
229+
230+ return new ArrayType (new MixedType (), new MixedType ());
231+ }
232+
233+ private function getSimpleObjectHydratedReturnType (Type $ queryResultType ): Type
234+ {
235+ if ((new ObjectWithoutClassType ())->isSuperTypeOf ($ queryResultType )->yes ()) {
236+ return $ queryResultType ;
237+ }
238+
239+ return new MixedType ();
240+ }
241+
242+ private function getSingleScalarHydratedReturnType (Type $ queryResultType ): Type
243+ {
244+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
245+ if (!$ queryResultType instanceof ConstantArrayType) {
246+ return new ArrayType (new MixedType (), new MixedType ());
247+ }
248+
249+ $ values = $ queryResultType ->getValueTypes ();
250+ if (count ($ values ) !== 1 ) {
251+ return new ArrayType (new MixedType (), new MixedType ());
252+ }
253+
254+ return $ queryResultType ;
255+ }
256+
257+ private function getScalarColumnHydratedReturnType (Type $ queryResultType ): Type
258+ {
259+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
260+ if (!$ queryResultType instanceof ConstantArrayType) {
261+ return new MixedType ();
262+ }
263+
264+ $ values = $ queryResultType ->getValueTypes ();
265+ if (count ($ values ) !== 1 ) {
266+ return new MixedType ();
168267 }
169268
170- return $ type -> getValue () === AbstractQuery:: HYDRATE_OBJECT ;
269+ return $ queryResultType -> getFirstIterableValueType () ;
171270 }
172271
173272 private function originalReturnType (MethodReflection $ methodReflection ): Type
0 commit comments