99use PHPStan \Reflection \ParametersAcceptorSelector ;
1010use PHPStan \ShouldNotHappenException ;
1111use PHPStan \Type \ArrayType ;
12+ use PHPStan \Type \Constant \ConstantArrayType ;
1213use PHPStan \Type \Constant \ConstantIntegerType ;
1314use PHPStan \Type \DynamicMethodReturnTypeExtension ;
1415use PHPStan \Type \Generic \GenericObjectType ;
1516use PHPStan \Type \IntegerType ;
1617use PHPStan \Type \IterableType ;
1718use PHPStan \Type \MixedType ;
1819use PHPStan \Type \NullType ;
20+ use PHPStan \Type \ObjectWithoutClassType ;
1921use PHPStan \Type \Type ;
2022use PHPStan \Type \TypeCombinator ;
23+ use PHPStan \Type \TypeTraverser ;
2124use PHPStan \Type \VoidType ;
25+ use function count ;
2226
2327final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
2428{
@@ -33,14 +37,22 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
3337 'getSingleResult ' => 0 ,
3438 ];
3539
40+ private const METHOD_HYDRATION_MODE = [
41+ 'getArrayResult ' => AbstractQuery::HYDRATE_ARRAY ,
42+ 'getScalarResult ' => AbstractQuery::HYDRATE_SCALAR ,
43+ 'getSingleColumnResult ' => AbstractQuery::HYDRATE_SCALAR_COLUMN ,
44+ 'getSingleScalarResult ' => AbstractQuery::HYDRATE_SINGLE_SCALAR ,
45+ ];
46+
3647 public function getClass (): string
3748 {
3849 return AbstractQuery::class;
3950 }
4051
4152 public function isMethodSupported (MethodReflection $ methodReflection ): bool
4253 {
43- return isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodReflection ->getName ()]);
54+ return isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodReflection ->getName ()])
55+ || isset (self ::METHOD_HYDRATION_MODE [$ methodReflection ->getName ()]);
4456 }
4557
4658 public function getTypeFromMethodCall (
@@ -51,21 +63,23 @@ public function getTypeFromMethodCall(
5163 {
5264 $ methodName = $ methodReflection ->getName ();
5365
54- if (!isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodName ])) {
55- throw new ShouldNotHappenException ();
56- }
57-
58- $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ];
59- $ args = $ methodCall ->getArgs ();
60-
61- if (isset ($ args [$ argIndex ])) {
62- $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
66+ if (isset (self ::METHOD_HYDRATION_MODE [$ methodName ])) {
67+ $ hydrationMode = new ConstantIntegerType (self ::METHOD_HYDRATION_MODE [$ methodName ]);
68+ } elseif (isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodName ])) {
69+ $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ];
70+ $ args = $ methodCall ->getArgs ();
71+
72+ if (isset ($ args [$ argIndex ])) {
73+ $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
74+ } else {
75+ $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
76+ $ methodReflection ->getVariants ()
77+ );
78+ $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
79+ $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
80+ }
6381 } else {
64- $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
65- $ methodReflection ->getVariants ()
66- );
67- $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
68- $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
82+ throw new ShouldNotHappenException ();
6983 }
7084
7185 $ queryType = $ scope ->getType ($ methodCall ->var );
@@ -109,12 +123,32 @@ private function getMethodReturnTypeForHydrationMode(
109123 return $ this ->originalReturnType ($ methodReflection );
110124 }
111125
112- if (!$ this ->isObjectHydrationMode ($ hydrationMode )) {
113- // We support only HYDRATE_OBJECT. For other hydration modes, we
114- // return the declared return type of the method.
126+ if (!$ hydrationMode instanceof ConstantIntegerType) {
115127 return $ this ->originalReturnType ($ methodReflection );
116128 }
117129
130+ switch ($ hydrationMode ->getValue ()) {
131+ case AbstractQuery::HYDRATE_OBJECT :
132+ break ;
133+ case AbstractQuery::HYDRATE_ARRAY :
134+ $ queryResultType = $ this ->getArrayHydratedReturnType ($ queryResultType );
135+ break ;
136+ case AbstractQuery::HYDRATE_SCALAR :
137+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
138+ break ;
139+ case AbstractQuery::HYDRATE_SINGLE_SCALAR :
140+ $ queryResultType = $ this ->getSingleScalarHydratedReturnType ($ queryResultType );
141+ break ;
142+ case AbstractQuery::HYDRATE_SIMPLEOBJECT :
143+ $ queryResultType = $ this ->getSimpleObjectHydratedReturnType ($ queryResultType );
144+ break ;
145+ case AbstractQuery::HYDRATE_SCALAR_COLUMN :
146+ $ queryResultType = $ this ->getScalarColumnHydratedReturnType ($ queryResultType );
147+ break ;
148+ default :
149+ return $ this ->originalReturnType ($ methodReflection );
150+ }
151+
118152 switch ($ methodReflection ->getName ()) {
119153 case 'getSingleResult ' :
120154 return $ queryResultType ;
@@ -133,13 +167,78 @@ private function getMethodReturnTypeForHydrationMode(
133167 }
134168 }
135169
136- private function isObjectHydrationMode (Type $ type ): bool
170+ private function getArrayHydratedReturnType (Type $ queryResultType ): Type
171+ {
172+ return TypeTraverser::map (
173+ $ queryResultType ,
174+ static function (Type $ type , callable $ traverse ): Type {
175+ $ isObject = (new ObjectWithoutClassType ())->isSuperTypeOf ($ type );
176+ if ($ isObject ->yes ()) {
177+ return new ArrayType (new MixedType (), new MixedType ());
178+ }
179+ if ($ isObject ->maybe ()) {
180+ return new MixedType ();
181+ }
182+
183+ return $ traverse ($ type );
184+ }
185+ );
186+ }
187+
188+ private function getScalarHydratedReturnType (Type $ queryResultType ): Type
137189 {
138- if (!$ type instanceof ConstantIntegerType) {
139- return false ;
190+ if (!$ queryResultType instanceof ArrayType) {
191+ return new ArrayType (new MixedType (), new MixedType ());
192+ }
193+
194+ $ itemType = $ queryResultType ->getItemType ();
195+ $ hasNoObject = (new ObjectWithoutClassType ())->isSuperTypeOf ($ itemType )->no ();
196+ $ hasNoArray = $ itemType ->isArray ()->no ();
197+
198+ if ($ hasNoArray && $ hasNoObject ) {
199+ return $ queryResultType ;
200+ }
201+
202+ return new ArrayType (new MixedType (), new MixedType ());
203+ }
204+
205+ private function getSimpleObjectHydratedReturnType (Type $ queryResultType ): Type
206+ {
207+ if ((new ObjectWithoutClassType ())->isSuperTypeOf ($ queryResultType )->yes ()) {
208+ return $ queryResultType ;
209+ }
210+
211+ return new MixedType ();
212+ }
213+
214+ private function getSingleScalarHydratedReturnType (Type $ queryResultType ): Type
215+ {
216+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
217+ if (!$ queryResultType instanceof ConstantArrayType) {
218+ return new ArrayType (new MixedType (), new MixedType ());
219+ }
220+
221+ $ values = $ queryResultType ->getValueTypes ();
222+ if (count ($ values ) !== 1 ) {
223+ return new ArrayType (new MixedType (), new MixedType ());
224+ }
225+
226+ return $ queryResultType ;
227+ }
228+
229+ private function getScalarColumnHydratedReturnType (Type $ queryResultType ): Type
230+ {
231+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
232+ if (!$ queryResultType instanceof ConstantArrayType) {
233+ return new MixedType ();
234+ }
235+
236+ $ values = $ queryResultType ->getValueTypes ();
237+ if (count ($ values ) !== 1 ) {
238+ return new MixedType ();
140239 }
141240
142- return $ type -> getValue () === AbstractQuery:: HYDRATE_OBJECT ;
241+ return $ queryResultType -> getFirstIterableValueType () ;
143242 }
144243
145244 private function originalReturnType (MethodReflection $ methodReflection ): Type
0 commit comments