88use PHPStan \Analyser \Scope ;
99use PHPStan \Reflection \MethodReflection ;
1010use PHPStan \Reflection \ParametersAcceptorSelector ;
11+ use PHPStan \ShouldNotHappenException ;
1112use PHPStan \Type \ArrayType ;
13+ use PHPStan \Type \BooleanType ;
14+ use PHPStan \Type \Constant \ConstantArrayType ;
1215use PHPStan \Type \Constant \ConstantIntegerType ;
16+ use PHPStan \Type \ConstantTypeHelper ;
1317use PHPStan \Type \DynamicMethodReturnTypeExtension ;
18+ use PHPStan \Type \FloatType ;
1419use PHPStan \Type \Generic \GenericObjectType ;
1520use PHPStan \Type \IntegerType ;
21+ use PHPStan \Type \MixedType ;
22+ use PHPStan \Type \NullType ;
23+ use PHPStan \Type \StringType ;
1624use PHPStan \Type \Type ;
1725use PHPStan \Type \TypeCombinator ;
1826use PHPStan \Type \VoidType ;
@@ -25,6 +33,7 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
2533 'execute ' => 1 ,
2634 'executeIgnoreQueryCache ' => 1 ,
2735 'executeUsingQueryCache ' => 1 ,
36+ 'getOneOrNullResult ' => 0 ,
2837 'getSingleResult ' => 0 ,
2938 ];
3039
@@ -45,69 +54,89 @@ public function getTypeFromMethodCall(
4554 ): Type
4655 {
4756 $ methodName = $ methodReflection ->getName ();
48- $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ] ?? 0 ;
4957
50- $ isHydrationModeObject = $ this ->isHydrationModeObject ($ methodCall , $ scope , $ argIndex );
58+ if (!isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodName ])) {
59+ throw new ShouldNotHappenException ();
60+ }
61+
62+ $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ];
63+ $ args = $ methodCall ->getArgs ();
5164
52- if (!$ isHydrationModeObject ) {
53- return $ this ->fallbackType ($ methodReflection );
65+ if (isset ($ args [$ argIndex ])) {
66+ $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
67+ } else {
68+ $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
69+ $ methodReflection ->getVariants ()
70+ );
71+ $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
72+ $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
5473 }
5574
5675 $ queryType = $ scope ->getType ($ methodCall ->var );
76+ $ queryResultType = $ this ->getQueryResultType ($ queryType );
77+
78+ return $ this ->getMethodReturnTypeForHydrationMode (
79+ $ methodReflection ,
80+ $ hydrationMode ,
81+ $ queryResultType ,
82+ );
83+ }
5784
85+ private function getQueryResultType (Type $ queryType ): Type
86+ {
5887 if (!$ queryType instanceof GenericObjectType) {
59- return $ this -> fallbackType ( $ methodReflection );
88+ return new MixedType ( );
6089 }
6190
6291 $ types = $ queryType ->getTypes ();
6392
64- if (!isset ($ types [0 ])) {
65- return $ this ->fallbackType ($ methodReflection );
66- }
67-
68- $ resultType = $ types [0 ];
93+ return $ types [0 ] ?? new MixedType ();
94+ }
6995
70- if ($ resultType instanceof VoidType) {
71- return $ this ->fallbackType ($ methodReflection );
96+ private function getMethodReturnTypeForHydrationMode (
97+ MethodReflection $ methodReflection ,
98+ Type $ hydrationMode ,
99+ Type $ queryResultType
100+ ): Type {
101+ if ($ queryResultType instanceof VoidType) {
102+ // A void query result type indicates an UPDATE or DELETE query.
103+ // In this case all methods return the number of affected rows.
104+ return new IntegerType ();
72105 }
73106
74- if ($ methodName === 'getOneOrNullResult ' ) {
75- return TypeCombinator::addNull ($ resultType );
107+ if (!$ this ->isObjectHydrationMode ($ hydrationMode )) {
108+ // We support only HYDRATE_OBJECT. For other hydration modes, we
109+ // return the declared return type of the method.
110+ return $ this ->originalReturnType ($ methodReflection );
76111 }
77112
78- if ($ methodName === 'getSingleResult ' ) {
79- return $ resultType ;
113+ switch ($ methodReflection ->getName ()) {
114+ case 'getSingleResult ' :
115+ return $ queryResultType ;
116+ case 'getOneOrNullResult ' :
117+ return TypeCombinator::addNull ($ queryResultType );
118+ default :
119+ return new ArrayType (
120+ new MixedType (),
121+ $ queryResultType
122+ );
80123 }
81-
82- return new ArrayType (
83- new IntegerType (),
84- $ resultType
85- );
86124 }
87125
88- private function isHydrationModeObject (
89- MethodCall $ methodCall ,
90- Scope $ scope ,
91- int $ argIndex
92- ): bool
126+ private function isObjectHydrationMode (Type $ type ): bool
93127 {
94- $ args = $ methodCall ->getArgs ();
95-
96- if (!isset ($ args [$ argIndex ])) {
97- return true ;
98- }
99-
100- $ argType = $ scope ->getType ($ args [$ argIndex ]->value );
101- if (!$ argType instanceof ConstantIntegerType) {
128+ if (!$ type instanceof ConstantIntegerType) {
102129 return false ;
103130 }
104131
105- return $ argType ->getValue () === Query ::HYDRATE_OBJECT ;
132+ return $ type ->getValue () === AbstractQuery ::HYDRATE_OBJECT ;
106133 }
107134
108- private function fallbackType (MethodReflection $ methodReflection ): Type
135+ private function originalReturnType (MethodReflection $ methodReflection ): Type
109136 {
110- $ parametersAcceptor = ParametersAcceptorSelector::selectSingle ($ methodReflection ->getVariants ());
137+ $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
138+ $ methodReflection ->getVariants ()
139+ );
111140
112141 return $ parametersAcceptor ->getReturnType ();
113142 }
0 commit comments