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 , $ enumType ] = $ 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 , $ enumType , $ nullable );
284285
285286 return $ this ->marshalType ($ fieldType );
286287
@@ -530,16 +531,13 @@ public function walkFunction($function)
530531 $ targetFieldName = $ function ->fieldMapping ;
531532 }
532533
533- $ typeName = $ targetClass ->getTypeOfField ($ targetFieldName );
534- if ($ typeName === null ) {
535- return $ this ->marshalType (new MixedType ());
536- }
537- 538534 $ fieldMapping = $ targetClass ->fieldMappings [$ targetFieldName ] ?? null ;
539535 if ($ fieldMapping === null ) {
540536 return $ this ->marshalType (new MixedType ());
541537 }
542538
539+ [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
540+ 543541 $ joinColumn = null ;
544542
545543 foreach ($ assoc ['joinColumns ' ] as $ item ) {
@@ -556,7 +554,7 @@ public function walkFunction($function)
556554 $ nullable = (bool ) ($ joinColumn ['nullable ' ] ?? true )
557555 || $ this ->hasAggregateWithoutGroupBy ();
558556
559- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ nullable );
557+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ nullable );
560558
561559 return $ this ->marshalType ($ fieldType );
562560
@@ -783,15 +781,13 @@ public function walkSelectExpression($selectExpression)
783781 $ qComp = $ this ->queryComponents [$ dqlAlias ];
784782 $ class = $ qComp ['metadata ' ];
785783
786- $ typeName = $ class ->getTypeOfField ($ fieldName );
787- 788- assert (is_string ($ typeName ));
784+ [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ class , $ fieldName );
789785
790786 $ nullable = $ this ->isQueryComponentNullable ($ dqlAlias )
791787 || $ class ->isNullable ($ fieldName )
792788 || $ this ->hasAggregateWithoutGroupBy ();
793789
794- $ type = $ this ->resolveDoctrineType ($ typeName , $ nullable );
790+ $ type = $ this ->resolveDoctrineType ($ typeName , $ enumType , $ nullable );
795791
796792 $ this ->typeBuilder ->addScalar ($ resultAlias , $ type );
797793
@@ -1295,14 +1291,37 @@ private function isQueryComponentNullable(string $dqlAlias): bool
12951291 return $ this ->nullableQueryComponents [$ dqlAlias ] ?? false ;
12961292 }
12971293
1298- private function resolveDoctrineType (string $ typeName , bool $ nullable = false ): Type
1294+ /** @return array{string, ?class-string<BackedEnum>} Doctrine type name and enum type of field */
1295+ private function getTypeOfField (ClassMetadataInfo $ class , string $ fieldName ): array
12991296 {
1300- try {
1301- $ type = $ this ->descriptorRegistry
1302- ->get ($ typeName )
1303- ->getWritableToPropertyType ();
1304- } catch (DescriptorNotRegisteredException $ e ) {
1305- $ type = new MixedType ();
1297+ assert (isset ($ class ->fieldMappings [$ fieldName ]));
1298+ 1299+ /** @var array{type: string, enumType?: ?string} $metadata */
1300+ $ metadata = $ class ->fieldMappings [$ fieldName ];
1301+ 1302+ $ type = $ metadata ['type ' ];
1303+ $ enumType = $ metadata ['enumType ' ] ?? null ;
1304+ 1305+ if (!is_string ($ enumType ) || !class_exists ($ enumType ) || !is_subclass_of ($ enumType , BackedEnum::class)) {
1306+ $ enumType = null ;
1307+ }
1308+ 1309+ return [$ type , $ enumType ];
1310+ }
1311+ 1312+ /** @param ?class-string<BackedEnum> $enumType */
1313+ private function resolveDoctrineType (string $ typeName , ?string $ enumType = null , bool $ nullable = false ): Type
1314+ {
1315+ if ($ enumType !== null ) {
1316+ $ type = new ObjectType ($ enumType );
1317+ } else {
1318+ try {
1319+ $ type = $ this ->descriptorRegistry
1320+ ->get ($ typeName )
1321+ ->getWritableToPropertyType ();
1322+ } catch (DescriptorNotRegisteredException $ e ) {
1323+ $ type = new MixedType ();
1324+ }
13061325 }
13071326
13081327 if ($ nullable ) {
@@ -1312,7 +1331,8 @@ private function resolveDoctrineType(string $typeName, bool $nullable = false):
13121331 return $ type ;
13131332 }
13141333
1315- private function resolveDatabaseInternalType (string $ typeName , bool $ nullable = false ): Type
1334+ /** @param ?class-string<BackedEnum> $enumType */
1335+ private function resolveDatabaseInternalType (string $ typeName , ?string $ enumType = null , bool $ nullable = false ): Type
13161336 {
13171337 try {
13181338 $ type = $ this ->descriptorRegistry
@@ -1322,6 +1342,15 @@ private function resolveDatabaseInternalType(string $typeName, bool $nullable =
13221342 $ type = new MixedType ();
13231343 }
13241344
1345+ if ($ enumType !== null ) {
1346+ $ enumTypes = array_map (static function ($ enumType ) {
1347+ return ConstantTypeHelper::getTypeFromValue ($ enumType ->value );
1348+ }, $ enumType ::cases ());
1349+ $ enumType = TypeCombinator::union (...$ enumTypes );
1350+ $ enumType = TypeCombinator::union ($ enumType , $ enumType ->toString ());
1351+ $ type = TypeCombinator::intersect ($ enumType , $ type );
1352+ }
1353+ 13251354 if ($ nullable ) {
13261355 $ type = TypeCombinator::addNull ($ type );
13271356 }
0 commit comments