1010use PHPStan \ShouldNotHappenException ;
1111use PHPStan \Type \Accessory \AccessoryArrayListType ;
1212use PHPStan \Type \ArrayType ;
13+ use PHPStan \Type \Constant \ConstantArrayType ;
1314use PHPStan \Type \Constant \ConstantIntegerType ;
15+ use PHPStan \Type \Doctrine \ObjectMetadataResolver ;
1416use PHPStan \Type \DynamicMethodReturnTypeExtension ;
1517use PHPStan \Type \IntegerType ;
1618use PHPStan \Type \IterableType ;
19+ use PHPStan \Type \MixedType ;
1720use PHPStan \Type \NullType ;
21+ use PHPStan \Type \ObjectWithoutClassType ;
1822use PHPStan \Type \Type ;
1923use PHPStan \Type \TypeCombinator ;
24+ use PHPStan \Type \TypeTraverser ;
25+ use PHPStan \Type \TypeWithClassName ;
2026use PHPStan \Type \VoidType ;
27+ use function count ;
2128
2229final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
2330{
@@ -32,14 +39,32 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
3239 'getSingleResult ' => 0 ,
3340 ];
3441
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+
3559 public function getClass (): string
3660 {
3761 return AbstractQuery::class;
3862 }
3963
4064 public function isMethodSupported (MethodReflection $ methodReflection ): bool
4165 {
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 ()]);
4368 }
4469
4570 public function getTypeFromMethodCall (
@@ -50,21 +75,23 @@ public function getTypeFromMethodCall(
5075 {
5176 $ methodName = $ methodReflection ->getName ();
5277
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 ();
5983
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+ }
6293 } else {
63- $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
64- $ methodReflection ->getVariants ()
65- );
66- $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
67- $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
94+ throw new ShouldNotHappenException ();
6895 }
6996
7097 $ queryType = $ scope ->getType ($ methodCall ->var );
@@ -98,12 +125,34 @@ private function getMethodReturnTypeForHydrationMode(
98125 return $ this ->originalReturnType ($ methodReflection );
99126 }
100127
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) {
104129 return $ this ->originalReturnType ($ methodReflection );
105130 }
106131
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+
107156 switch ($ methodReflection ->getName ()) {
108157 case 'getSingleResult ' :
109158 return $ queryResultType ;
@@ -115,6 +164,10 @@ private function getMethodReturnTypeForHydrationMode(
115164 $ queryResultType
116165 );
117166 default :
167+ if ($ singleResult ) {
168+ return $ queryResultType ;
169+ }
170+
118171 if ($ queryKeyType ->isNull ()->yes ()) {
119172 return AccessoryArrayListType::intersectWith (new ArrayType (
120173 new IntegerType (),
@@ -128,13 +181,86 @@ private function getMethodReturnTypeForHydrationMode(
128181 }
129182 }
130183
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
132228 {
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 ();
135261 }
136262
137- return $ type -> getValue () === AbstractQuery:: HYDRATE_OBJECT ;
263+ return $ queryResultType -> getFirstIterableValueType () ;
138264 }
139265
140266 private function originalReturnType (MethodReflection $ methodReflection ): Type
0 commit comments