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,58 @@ private function getMethodReturnTypeForHydrationMode(
98126 return null ;
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) {
130+ return null ;
131+ }
132+
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 null ;
155+ }
156+
157+ if ($ queryResultType === null ) {
104158 return null ;
105159 }
106160
107161 switch ($ methodReflection ->getName ()) {
108162 case 'getSingleResult ' :
109163 return $ queryResultType ;
110164 case 'getOneOrNullResult ' :
111- return TypeCombinator::addNull ($ queryResultType );
165+ $ nullableQueryResultType = TypeCombinator::addNull ($ queryResultType );
166+ if ($ queryResultType instanceof BenevolentUnionType) {
167+ $ nullableQueryResultType = TypeUtils::toBenevolentUnion ($ nullableQueryResultType );
168+ }
169+
170+ return $ nullableQueryResultType ;
112171 case 'toIterable ' :
113172 return new IterableType (
114173 $ queryKeyType ->isNull ()->yes () ? new IntegerType () : $ queryKeyType ,
115174 $ queryResultType
116175 );
117176 default :
177+ if ($ singleResult ) {
178+ return $ queryResultType ;
179+ }
180+
118181 if ($ queryKeyType ->isNull ()->yes ()) {
119182 return AccessoryArrayListType::intersectWith (new ArrayType (
120183 new IntegerType (),
@@ -128,13 +191,127 @@ private function getMethodReturnTypeForHydrationMode(
128191 }
129192 }
130193
131- private function isObjectHydrationMode (Type $ type ): bool
194+ /**
195+ * When we're array-hydrating object, we're not sure of the shape of the array.
196+ * We could return `new ArrayTyp(new MixedType(), new MixedType())`
197+ * but the lack of precision in the array keys/values would give false positive.
198+ *
199+ * @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934
200+ */
201+ private function getArrayHydratedReturnType (Type $ queryResultType ): ?Type
132202 {
133- if (!$ type instanceof ConstantIntegerType) {
134- return false ;
203+ $ objectManager = $ this ->objectMetadataResolver ->getObjectManager ();
204+
205+ $ mixedFound = false ;
206+ $ queryResultType = TypeTraverser::map (
207+ $ queryResultType ,
208+ static function (Type $ type , callable $ traverse ) use ($ objectManager , &$ mixedFound ): Type {
209+ $ isObject = (new ObjectWithoutClassType ())->isSuperTypeOf ($ type );
210+ if ($ isObject ->no ()) {
211+ return $ traverse ($ type );
212+ }
213+ if (
214+ $ isObject ->maybe ()
215+ || !$ type instanceof TypeWithClassName
216+ || $ objectManager === null
217+ ) {
218+ $ mixedFound = true ;
219+
220+ return new MixedType ();
221+ }
222+
223+ /** @var class-string $className */
224+ $ className = $ type ->getClassName ();
225+ if (!$ objectManager ->getMetadataFactory ()->hasMetadataFor ($ className )) {
226+ return $ traverse ($ type );
227+ }
228+
229+ $ mixedFound = true ;
230+
231+ return new MixedType ();
232+ }
233+ );
234+
235+ return $ mixedFound ? null : $ queryResultType ;
236+ }
237+
238+ /**
239+ * When we're scalar-hydrating object, we're not sure of the shape of the array.
240+ * We could return `new ArrayTyp(new MixedType(), new MixedType())`
241+ * but the lack of precision in the array keys/values would give false positive.
242+ *
243+ * @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544
244+ */
245+ private function getScalarHydratedReturnType (Type $ queryResultType ): ?Type
246+ {
247+ if (!$ queryResultType ->isArray ()->yes ()) {
248+ return null ;
249+ }
250+
251+ foreach ($ queryResultType ->getArrays () as $ arrayType ) {
252+ $ itemType = $ arrayType ->getItemType ();
253+
254+ if (
255+ !(new ObjectWithoutClassType ())->isSuperTypeOf ($ itemType )->no ()
256+ || !$ itemType ->isArray ()->no ()
257+ ) {
258+ // We could return `new ArrayTyp(new MixedType(), new MixedType())`
259+ // but the lack of precision in the array keys/values would give false positive
260+ // @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544
261+ return null ;
262+ }
263+ }
264+
265+ return $ queryResultType ;
266+ }
267+
268+ private function getSimpleObjectHydratedReturnType (Type $ queryResultType ): ?Type
269+ {
270+ if ((new ObjectWithoutClassType ())->isSuperTypeOf ($ queryResultType )->yes ()) {
271+ return $ queryResultType ;
272+ }
273+
274+ return null ;
275+ }
276+
277+ private function getSingleScalarHydratedReturnType (Type $ queryResultType ): ?Type
278+ {
279+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
280+ if ($ queryResultType === null || !$ queryResultType ->isConstantArray ()->yes ()) {
281+ return null ;
282+ }
283+
284+ $ types = [];
285+ foreach ($ queryResultType ->getConstantArrays () as $ constantArrayType ) {
286+ $ values = $ constantArrayType ->getValueTypes ();
287+ if (count ($ values ) !== 1 ) {
288+ return null ;
289+ }
290+
291+ $ types [] = $ constantArrayType ->getFirstIterableValueType ();
292+ }
293+
294+ return TypeCombinator::union (...$ types );
295+ }
296+
297+ private function getScalarColumnHydratedReturnType (Type $ queryResultType ): ?Type
298+ {
299+ $ queryResultType = $ this ->getScalarHydratedReturnType ($ queryResultType );
300+ if ($ queryResultType === null || !$ queryResultType ->isConstantArray ()->yes ()) {
301+ return null ;
302+ }
303+
304+ $ types = [];
305+ foreach ($ queryResultType ->getConstantArrays () as $ constantArrayType ) {
306+ $ values = $ constantArrayType ->getValueTypes ();
307+ if (count ($ values ) !== 1 ) {
308+ return null ;
309+ }
310+
311+ $ types [] = $ constantArrayType ->getFirstIterableValueType ();
135312 }
136313
137- return $ type -> getValue () === AbstractQuery:: HYDRATE_OBJECT ;
314+ return TypeCombinator:: union (... $ types ) ;
138315 }
139316
140317}
0 commit comments