33namespace PHPStan \Type \Doctrine \Query ;
44
55use Doctrine \ORM \AbstractQuery ;
6- use Doctrine \ORM \Query ;
76use PhpParser \Node \Expr \MethodCall ;
87use PHPStan \Analyser \Scope ;
98use PHPStan \Reflection \MethodReflection ;
109use PHPStan \Reflection \ParametersAcceptorSelector ;
10+ use PHPStan \ShouldNotHappenException ;
1111use PHPStan \Type \ArrayType ;
1212use PHPStan \Type \Constant \ConstantIntegerType ;
1313use PHPStan \Type \DynamicMethodReturnTypeExtension ;
1414use PHPStan \Type \Generic \GenericObjectType ;
1515use PHPStan \Type \IntegerType ;
16+ use PHPStan \Type \MixedType ;
17+ use PHPStan \Type \NullType ;
1618use PHPStan \Type \Type ;
1719use PHPStan \Type \TypeCombinator ;
1820use PHPStan \Type \VoidType ;
@@ -25,6 +27,7 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
2527 'execute ' => 1 ,
2628 'executeIgnoreQueryCache ' => 1 ,
2729 'executeUsingQueryCache ' => 1 ,
30+ 'getOneOrNullResult ' => 0 ,
2831 'getSingleResult ' => 0 ,
2932 ];
3033
@@ -45,69 +48,90 @@ public function getTypeFromMethodCall(
4548 ): Type
4649 {
4750 $ methodName = $ methodReflection ->getName ();
48- $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ] ?? 0 ;
4951
50- $ isHydrationModeObject = $ this ->isHydrationModeObject ($ methodCall , $ scope , $ argIndex );
52+ if (!isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodName ])) {
53+ throw new ShouldNotHappenException ();
54+ }
55+
56+ $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ];
57+ $ args = $ methodCall ->getArgs ();
5158
52- if (!$ isHydrationModeObject ) {
53- return $ this ->fallbackType ($ methodReflection );
59+ if (isset ($ args [$ argIndex ])) {
60+ $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
61+ } else {
62+ $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
63+ $ methodReflection ->getVariants ()
64+ );
65+ $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
66+ $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
5467 }
5568
5669 $ queryType = $ scope ->getType ($ methodCall ->var );
70+ $ queryResultType = $ this ->getQueryResultType ($ queryType );
71+
72+ return $ this ->getMethodReturnTypeForHydrationMode (
73+ $ methodReflection ,
74+ $ hydrationMode ,
75+ $ queryResultType ,
76+ );
77+ }
5778
79+ private function getQueryResultType (Type $ queryType ): Type
80+ {
5881 if (!$ queryType instanceof GenericObjectType) {
59- return $ this -> fallbackType ( $ methodReflection );
82+ return new MixedType ( );
6083 }
6184
6285 $ types = $ queryType ->getTypes ();
6386
64- if (!isset ($ types [0 ])) {
65- return $ this ->fallbackType ($ methodReflection );
66- }
67-
68- $ resultType = $ types [0 ];
87+ return $ types [0 ] ?? new MixedType ();
88+ }
6989
70- if ($ resultType instanceof VoidType) {
71- return $ this ->fallbackType ($ methodReflection );
90+ private function getMethodReturnTypeForHydrationMode (
91+ MethodReflection $ methodReflection ,
92+ Type $ hydrationMode ,
93+ Type $ queryResultType
94+ ): Type
95+ {
96+ if ($ queryResultType instanceof VoidType) {
97+ // A void query result type indicates an UPDATE or DELETE query.
98+ // In this case all methods return the number of affected rows.
99+ return new IntegerType ();
72100 }
73101
74- if ($ methodName === 'getOneOrNullResult ' ) {
75- return TypeCombinator::addNull ($ resultType );
102+ if (!$ this ->isObjectHydrationMode ($ hydrationMode )) {
103+ // We support only HYDRATE_OBJECT. For other hydration modes, we
104+ // return the declared return type of the method.
105+ return $ this ->originalReturnType ($ methodReflection );
76106 }
77107
78- if ($ methodName === 'getSingleResult ' ) {
79- return $ resultType ;
108+ switch ($ methodReflection ->getName ()) {
109+ case 'getSingleResult ' :
110+ return $ queryResultType ;
111+ case 'getOneOrNullResult ' :
112+ return TypeCombinator::addNull ($ queryResultType );
113+ default :
114+ return new ArrayType (
115+ new MixedType (),
116+ $ queryResultType
117+ );
80118 }
81-
82- return new ArrayType (
83- new IntegerType (),
84- $ resultType
85- );
86119 }
87120
88- private function isHydrationModeObject (
89- MethodCall $ methodCall ,
90- Scope $ scope ,
91- int $ argIndex
92- ): bool
121+ private function isObjectHydrationMode (Type $ type ): bool
93122 {
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) {
123+ if (!$ type instanceof ConstantIntegerType) {
102124 return false ;
103125 }
104126
105- return $ argType ->getValue () === Query ::HYDRATE_OBJECT ;
127+ return $ type ->getValue () === AbstractQuery ::HYDRATE_OBJECT ;
106128 }
107129
108- private function fallbackType (MethodReflection $ methodReflection ): Type
130+ private function originalReturnType (MethodReflection $ methodReflection ): Type
109131 {
110- $ parametersAcceptor = ParametersAcceptorSelector::selectSingle ($ methodReflection ->getVariants ());
132+ $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
133+ $ methodReflection ->getVariants ()
134+ );
111135
112136 return $ parametersAcceptor ->getReturnType ();
113137 }
0 commit comments