1010use PHPStan \ShouldNotHappenException ;
1111use PHPStan \Type \Accessory \AccessoryArrayListType ;
1212use PHPStan \Type \ArrayType ;
13+ use PHPStan \Type \BenevolentUnionType ;
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 \TypeUtils ;
26+ use PHPStan \Type \TypeWithClassName ;
2027use PHPStan \Type \VoidType ;
28+ use function count ;
2129
2230final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
2331{
@@ -32,14 +40,32 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
3240 'getSingleResult ' => 0 ,
3341 ];
3442
43+ private const METHOD_HYDRATION_MODE = [
44+ 'getArrayResult ' => AbstractQuery::HYDRATE_ARRAY ,
45+ 'getScalarResult ' => AbstractQuery::HYDRATE_SCALAR ,
46+ 'getSingleColumnResult ' => AbstractQuery::HYDRATE_SCALAR_COLUMN ,
47+ 'getSingleScalarResult ' => AbstractQuery::HYDRATE_SINGLE_SCALAR ,
48+ ];
49+
50+ /** @var ObjectMetadataResolver */
51+ private $ objectMetadataResolver ;
52+
53+ public function __construct (
54+ ObjectMetadataResolver $ objectMetadataResolver
55+ )
56+ {
57+ $ this ->objectMetadataResolver = $ objectMetadataResolver ;
58+ }
59+
3560 public function getClass (): string
3661 {
3762 return AbstractQuery::class;
3863 }
3964
4065 public function isMethodSupported (MethodReflection $ methodReflection ): bool
4166 {
42- return isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodReflection ->getName ()]);
67+ return isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodReflection ->getName ()])
68+ || isset (self ::METHOD_HYDRATION_MODE [$ methodReflection ->getName ()]);
4369 }
4470
4571 public function getTypeFromMethodCall (
@@ -50,21 +76,23 @@ public function getTypeFromMethodCall(
5076 {
5177 $ methodName = $ methodReflection ->getName ();
5278
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 ();
79+ if (isset (self ::METHOD_HYDRATION_MODE [$ methodName ])) {
80+ $ hydrationMode = new ConstantIntegerType (self ::METHOD_HYDRATION_MODE [$ methodName ]);
81+ } elseif (isset (self ::METHOD_HYDRATION_MODE_ARG [$ methodName ])) {
82+ $ argIndex = self ::METHOD_HYDRATION_MODE_ARG [$ methodName ];
83+ $ args = $ methodCall ->getArgs ();
5984
60- if (isset ($ args [$ argIndex ])) {
61- $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
85+ if (isset ($ args [$ argIndex ])) {
86+ $ hydrationMode = $ scope ->getType ($ args [$ argIndex ]->value );
87+ } else {
88+ $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
89+ $ methodReflection ->getVariants ()
90+ );
91+ $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
92+ $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
93+ }
6294 } else {
63- $ parametersAcceptor = ParametersAcceptorSelector::selectSingle (
64- $ methodReflection ->getVariants ()
65- );
66- $ parameter = $ parametersAcceptor ->getParameters ()[$ argIndex ];
67- $ hydrationMode = $ parameter ->getDefaultValue () ?? new NullType ();
95+ throw new ShouldNotHappenException ();
6896 }
6997
7098 $ queryType = $ scope ->getType ($ methodCall ->var );
@@ -98,23 +126,54 @@ private function getMethodReturnTypeForHydrationMode(
98126 return $ this ->originalReturnType ($ methodReflection );
99127 }
100128
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.
129+ if (!$ hydrationMode instanceof ConstantIntegerType) {
104130 return $ this ->originalReturnType ($ methodReflection );
105131 }
106132
133+ $ singleResult = false ;
134+ switch ($ hydrationMode ->getValue ()) {
135+ case AbstractQuery::HYDRATE_OBJECT :
136+ break ;
137+ case AbstractQuery::HYDRATE_ARRAY :
138+ $ queryResultType = $ this ->getArrayHydratedReturnType ($ queryResultType );
139+ break ;
140+ case AbstractQuery::HYDRATE_SCALAR :
141+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
142+ break ;
143+ case AbstractQuery::HYDRATE_SINGLE_SCALAR :
144+ $ singleResult = true ;
145+ $ queryResultType = $ this ->getSingleScalarHydratedReturnType ($ queryResultType );
146+ break ;
147+ case AbstractQuery::HYDRATE_SIMPLEOBJECT :
148+ $ queryResultType = $ this ->getSimpleObjectHydratedReturnType ($ queryResultType );
149+ break ;
150+ case AbstractQuery::HYDRATE_SCALAR_COLUMN :
151+ $ queryResultType = $ this ->getScalarColumnHydratedReturnType ($ queryResultType );
152+ break ;
153+ default :
154+ return $ this ->originalReturnType ($ methodReflection );
155+ }
156+
107157 switch ($ methodReflection ->getName ()) {
108158 case 'getSingleResult ' :
109159 return $ queryResultType ;
110160 case 'getOneOrNullResult ' :
111- return TypeCombinator::addNull ($ queryResultType );
161+ $ nullableQueryResultType = TypeCombinator::addNull ($ queryResultType );
162+ if ($ queryResultType instanceof BenevolentUnionType) {
163+ $ nullableQueryResultType = TypeUtils::toBenevolentUnion ($ nullableQueryResultType );
164+ }
165+
166+ return $ nullableQueryResultType ;
112167 case 'toIterable ' :
113168 return new IterableType (
114169 $ queryKeyType ->isNull ()->yes () ? new IntegerType () : $ queryKeyType ,
115170 $ queryResultType
116171 );
117172 default :
173+ if ($ singleResult ) {
174+ return $ queryResultType ;
175+ }
176+
118177 if ($ queryKeyType ->isNull ()->yes ()) {
119178 return AccessoryArrayListType::intersectWith (new ArrayType (
120179 new IntegerType (),
@@ -128,13 +187,104 @@ private function getMethodReturnTypeForHydrationMode(
128187 }
129188 }
130189
131- private function isObjectHydrationMode (Type $ type ): bool
190+ private function getArrayHydratedReturnType (Type $ queryResultType ): Type
191+ {
192+ $ objectManager = $ this ->objectMetadataResolver ->getObjectManager ();
193+
194+ return TypeTraverser::map (
195+ $ queryResultType ,
196+ static function (Type $ type , callable $ traverse ) use ($ objectManager ): Type {
197+ $ isObject = (new ObjectWithoutClassType ())->isSuperTypeOf ($ type );
198+ if ($ isObject ->no ()) {
199+ return $ traverse ($ type );
200+ }
201+ if (
202+ $ isObject ->maybe ()
203+ || !$ type instanceof TypeWithClassName
204+ || $ objectManager === null
205+ ) {
206+ return new MixedType ();
207+ }
208+
209+ if (!$ objectManager ->getMetadataFactory ()->hasMetadataFor ($ type ->getClassName ())) {
210+ return $ traverse ($ type );
211+ }
212+
213+ // We could return `new ArrayTyp(new MixedType(), new MixedType())`
214+ // but the lack of precision in the array keys/values would give false positive
215+ // @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934
216+ return new MixedType ();
217+ }
218+ );
219+ }
220+
221+ private function getScalarHydratedReturnType (Type $ queryResultType ): Type
222+ {
223+ if (!$ queryResultType ->isArray ()->yes ()) {
224+ return new ArrayType (new MixedType (), new MixedType ());
225+ }
226+
227+ foreach ($ queryResultType ->getArrays () as $ arrayType ) {
228+ $ itemType = $ arrayType ->getItemType ();
229+
230+ if (
231+ !(new ObjectWithoutClassType ())->isSuperTypeOf ($ itemType )->no ()
232+ || !$ itemType ->isArray ()->no ()
233+ ) {
234+ return new ArrayType (new MixedType (), new MixedType ());
235+ }
236+ }
237+
238+ return $ queryResultType ;
239+ }
240+
241+ private function getSimpleObjectHydratedReturnType (Type $ queryResultType ): Type
242+ {
243+ if ((new ObjectWithoutClassType ())->isSuperTypeOf ($ queryResultType )->yes ()) {
244+ return $ queryResultType ;
245+ }
246+
247+ return new MixedType ();
248+ }
249+
250+ private function getSingleScalarHydratedReturnType (Type $ queryResultType ): Type
132251 {
133- if (!$ type instanceof ConstantIntegerType) {
134- return false ;
252+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
253+ if (!$ queryResultType ->isConstantArray ()->yes ()) {
254+ return new MixedType ();
255+ }
256+
257+ $ types = [];
258+ foreach ($ queryResultType ->getConstantArrays () as $ constantArrayType ) {
259+ $ values = $ constantArrayType ->getValueTypes ();
260+ if (count ($ values ) !== 1 ) {
261+ return new MixedType ();
262+ }
263+
264+ $ types [] = $ constantArrayType ->getFirstIterableValueType ();
265+ }
266+
267+ return TypeCombinator::union (...$ types );
268+ }
269+
270+ private function getScalarColumnHydratedReturnType (Type $ queryResultType ): Type
271+ {
272+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
273+ if (!$ queryResultType ->isConstantArray ()->yes ()) {
274+ return new MixedType ();
275+ }
276+
277+ $ types = [];
278+ foreach ($ queryResultType ->getConstantArrays () as $ constantArrayType ) {
279+ $ values = $ constantArrayType ->getValueTypes ();
280+ if (count ($ values ) !== 1 ) {
281+ return new MixedType ();
282+ }
283+
284+ $ types [] = $ constantArrayType ->getFirstIterableValueType ();
135285 }
136286
137- return $ type -> getValue () === AbstractQuery:: HYDRATE_OBJECT ;
287+ return TypeCombinator:: union (... $ types ) ;
138288 }
139289
140290 private function originalReturnType (MethodReflection $ methodReflection ): Type
0 commit comments