22
33namespace PHPStan \Type \Doctrine \Query ;
44
5+ use BackedEnum ;
56use Doctrine \ORM \EntityManagerInterface ;
67use Doctrine \ORM \Mapping \ClassMetadata ;
8+ use Doctrine \ORM \Mapping \ClassMetadataInfo ;
79use Doctrine \ORM \Query ;
810use Doctrine \ORM \Query \AST ;
911use Doctrine \ORM \Query \AST \TypedExpression ;
1517use PHPStan \Type \Constant \ConstantFloatType ;
1618use PHPStan \Type \Constant \ConstantIntegerType ;
1719use PHPStan \Type \Constant \ConstantStringType ;
20+ use PHPStan \Type \ConstantTypeHelper ;
1821use PHPStan \Type \Doctrine \DescriptorNotRegisteredException ;
1922use PHPStan \Type \Doctrine \DescriptorRegistry ;
2023use PHPStan \Type \FloatType ;
3134use PHPStan \Type \TypeTraverser ;
3235use PHPStan \Type \TypeUtils ;
3336use PHPStan \Type \UnionType ;
37+ use function array_map ;
3438use function assert ;
3539use function class_exists ;
3640use function count ;
4246use function is_numeric ;
4347use function is_object ;
4448use function is_string ;
49+ use function is_subclass_of ;
4550use function serialize ;
4651use function sprintf ;
4752use function strtolower ;
@@ -231,15 +236,13 @@ public function walkPathExpression($pathExpr)
231236
232237 switch ($ pathExpr ->type ) {
233238 case AST \PathExpression::TYPE_STATE_FIELD :
234- $ typeName = $ class ->getTypeOfField ($ fieldName );
235-
236- assert (is_string ($ typeName ));
239+ [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ class , $ fieldName );
237240
238241 $ nullable = $ this ->isQueryComponentNullable ($ dqlAlias )
239242 || $ class ->isNullable ($ fieldName )
240243 || $ this ->hasAggregateWithoutGroupBy ();
241244
242- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ nullable );
245+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ nullable );
243246
244247 return $ this ->marshalType ($ fieldType );
245248
@@ -273,14 +276,12 @@ public function walkPathExpression($pathExpr)
273276 }
274277
275278 $ targetFieldName = $ identifierFieldNames [0 ];
276- $ typeName = $ targetClass ->getTypeOfField ($ targetFieldName );
277-
278- assert (is_string ($ typeName ));
279+ [$ typeName ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
279280
280281 $ nullable = (bool ) ($ joinColumn ['nullable ' ] ?? true )
281282 || $ this ->hasAggregateWithoutGroupBy ();
282283
283- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ nullable );
284+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , null , $ nullable );
284285
285286 return $ this ->marshalType ($ fieldType );
286287
@@ -556,7 +557,7 @@ public function walkFunction($function)
556557 $ nullable = (bool ) ($ joinColumn ['nullable ' ] ?? true )
557558 || $ this ->hasAggregateWithoutGroupBy ();
558559
559- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ nullable );
560+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , null , $ nullable );
560561
561562 return $ this ->marshalType ($ fieldType );
562563
@@ -783,15 +784,13 @@ public function walkSelectExpression($selectExpression)
783784 $ qComp = $ this ->queryComponents [$ dqlAlias ];
784785 $ class = $ qComp ['metadata ' ];
785786
786- $ typeName = $ class ->getTypeOfField ($ fieldName );
787-
788- assert (is_string ($ typeName ));
787+ [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ class , $ fieldName );
789788
790789 $ nullable = $ this ->isQueryComponentNullable ($ dqlAlias )
791790 || $ class ->isNullable ($ fieldName )
792791 || $ this ->hasAggregateWithoutGroupBy ();
793792
794- $ type = $ this ->resolveDoctrineType ($ typeName , $ nullable );
793+ $ type = $ this ->resolveDoctrineType ($ typeName , $ enumType , $ nullable );
795794
796795 $ this ->typeBuilder ->addScalar ($ resultAlias , $ type );
797796
@@ -1295,14 +1294,37 @@ private function isQueryComponentNullable(string $dqlAlias): bool
12951294 return $ this ->nullableQueryComponents [$ dqlAlias ] ?? false ;
12961295 }
12971296
1298- private function resolveDoctrineType (string $ typeName , bool $ nullable = false ): Type
1297+ /** @return array{string, ?class-string<BackedEnum>} Doctrine type name and enum type of field */
1298+ private function getTypeOfField (ClassMetadataInfo $ class , string $ fieldName ): array
12991299 {
1300- try {
1301- $ type = $ this ->descriptorRegistry
1302- ->get ($ typeName )
1303- ->getWritableToPropertyType ();
1304- } catch (DescriptorNotRegisteredException $ e ) {
1305- $ type = new MixedType ();
1300+ assert (isset ($ class ->fieldMappings [$ fieldName ]));
1301+
1302+ /** @var array{type: string, enumType?: ?string} $metadata */
1303+ $ metadata = $ class ->fieldMappings [$ fieldName ];
1304+
1305+ $ type = $ metadata ['type ' ];
1306+ $ enumType = $ metadata ['enumType ' ] ?? null ;
1307+
1308+ if (!is_string ($ enumType ) || !class_exists ($ enumType ) || !is_subclass_of ($ enumType , BackedEnum::class)) {
1309+ $ enumType = null ;
1310+ }
1311+
1312+ return [$ type , $ enumType ];
1313+ }
1314+
1315+ /** @param ?class-string<BackedEnum> $enumType */
1316+ private function resolveDoctrineType (string $ typeName , ?string $ enumType = null , bool $ nullable = false ): Type
1317+ {
1318+ if ($ enumType !== null ) {
1319+ $ type = new ObjectType ($ enumType );
1320+ } else {
1321+ try {
1322+ $ type = $ this ->descriptorRegistry
1323+ ->get ($ typeName )
1324+ ->getWritableToPropertyType ();
1325+ } catch (DescriptorNotRegisteredException $ e ) {
1326+ $ type = new MixedType ();
1327+ }
13061328 }
13071329
13081330 if ($ nullable ) {
@@ -1312,7 +1334,8 @@ private function resolveDoctrineType(string $typeName, bool $nullable = false):
13121334 return $ type ;
13131335 }
13141336
1315- private function resolveDatabaseInternalType (string $ typeName , bool $ nullable = false ): Type
1337+ /** @param ?class-string<BackedEnum> $enumType */
1338+ private function resolveDatabaseInternalType (string $ typeName , ?string $ enumType = null , bool $ nullable = false ): Type
13161339 {
13171340 try {
13181341 $ type = $ this ->descriptorRegistry
@@ -1322,6 +1345,15 @@ private function resolveDatabaseInternalType(string $typeName, bool $nullable =
13221345 $ type = new MixedType ();
13231346 }
13241347
1348+ if ($ enumType !== null ) {
1349+ $ enumTypes = array_map (static function ($ enumType ) {
1350+ return ConstantTypeHelper::getTypeFromValue ($ enumType ->value );
1351+ }, $ enumType ::cases ());
1352+ $ enumType = TypeCombinator::union (...$ enumTypes );
1353+ $ enumType = TypeCombinator::union ($ enumType , $ enumType ->toString ());
1354+ $ type = TypeCombinator::intersect ($ enumType , $ type );
1355+ }
1356+
13251357 if ($ nullable ) {
13261358 $ type = TypeCombinator::addNull ($ type );
13271359 }
0 commit comments