33namespace PHPStan \Type \Doctrine \Query ;
44
55use BackedEnum ;
6+ use Doctrine \DBAL \Types \EnumType as DbalEnumType ;
67use Doctrine \DBAL \Types \StringType as DbalStringType ;
78use Doctrine \DBAL \Types \Type as DbalType ;
89use Doctrine \ORM \EntityManagerInterface ;
2627use PHPStan \Type \Constant \ConstantBooleanType ;
2728use PHPStan \Type \Constant \ConstantFloatType ;
2829use PHPStan \Type \Constant \ConstantIntegerType ;
30+ use PHPStan \Type \Constant \ConstantStringType ;
2931use PHPStan \Type \ConstantTypeHelper ;
3032use PHPStan \Type \Doctrine \DescriptorNotRegisteredException ;
3133use PHPStan \Type \Doctrine \DescriptorRegistry ;
5355use function get_class ;
5456use function gettype ;
5557use function in_array ;
58+ use function is_array ;
5659use function is_int ;
5760use function is_numeric ;
5861use function is_object ;
@@ -286,13 +289,13 @@ public function walkPathExpression($pathExpr): string
286289
287290 switch ($ pathExpr ->type ) {
288291 case AST \PathExpression::TYPE_STATE_FIELD :
289- [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ class , $ fieldName );
292+ [$ typeName , $ enumType, $ enumValues ] = $ this ->getTypeOfField ($ class , $ fieldName );
290293
291294 $ nullable = $ this ->isQueryComponentNullable ($ dqlAlias )
292295 || $ class ->isNullable ($ fieldName )
293296 || $ this ->hasAggregateWithoutGroupBy ();
294297
295- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ nullable );
298+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ enumValues , $ nullable );
296299
297300 return $ this ->marshalType ($ fieldType );
298301
@@ -326,12 +329,12 @@ public function walkPathExpression($pathExpr): string
326329 }
327330
328331 $ targetFieldName = $ identifierFieldNames [0 ];
329- [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
332+ [$ typeName , $ enumType, $ enumValues ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
330333
331334 $ nullable = ($ joinColumn ['nullable ' ] ?? true )
332335 || $ this ->hasAggregateWithoutGroupBy ();
333336
334- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ nullable );
337+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ enumValues , $ nullable );
335338
336339 return $ this ->marshalType ($ fieldType );
337340
@@ -685,7 +688,7 @@ public function walkFunction($function): string
685688 return $ this ->marshalType (new MixedType ());
686689 }
687690
688- [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
691+ [$ typeName , $ enumType, $ enumValues ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
689692
690693 if (!isset ($ assoc ['joinColumns ' ])) {
691694 return $ this ->marshalType (new MixedType ());
@@ -708,7 +711,7 @@ public function walkFunction($function): string
708711 || $ this ->isQueryComponentNullable ($ dqlAlias )
709712 || $ this ->hasAggregateWithoutGroupBy ();
710713
711- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ nullable );
714+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ enumValues , $ nullable );
712715
713716 return $ this ->marshalType ($ fieldType );
714717
@@ -1206,13 +1209,13 @@ public function walkSelectExpression($selectExpression): string
12061209 assert (array_key_exists ('metadata ' , $ qComp ));
12071210 $ class = $ qComp ['metadata ' ];
12081211
1209- [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ class , $ fieldName );
1212+ [$ typeName , $ enumType, $ enumValues ] = $ this ->getTypeOfField ($ class , $ fieldName );
12101213
12111214 $ nullable = $ this ->isQueryComponentNullable ($ dqlAlias )
12121215 || $ class ->isNullable ($ fieldName )
12131216 || $ this ->hasAggregateWithoutGroupBy ();
12141217
1215- $ type = $ this ->resolveDoctrineType ($ typeName , $ enumType , $ nullable );
1218+ $ type = $ this ->resolveDoctrineType ($ typeName , $ enumType , $ enumValues , $ nullable );
12161219
12171220 $ this ->typeBuilder ->addScalar ($ resultAlias , $ type );
12181221
@@ -1235,11 +1238,12 @@ public function walkSelectExpression($selectExpression): string
12351238 if (
12361239 $ expr instanceof TypedExpression
12371240 && !$ expr ->getReturnType () instanceof DbalStringType // StringType is no-op, so using TypedExpression with that does nothing
1241+ && !$ expr ->getReturnType () instanceof DbalEnumType // EnumType is also no-op
12381242 ) {
12391243 $ dbalTypeName = DbalType::getTypeRegistry ()->lookupName ($ expr ->getReturnType ());
12401244 $ type = TypeCombinator::intersect ( // e.g. count is typed as int, but we infer int<0, max>
12411245 $ type ,
1242- $ this ->resolveDoctrineType ($ dbalTypeName , null , TypeCombinator::containsNull ($ type )),
1246+ $ this ->resolveDoctrineType ($ dbalTypeName , null , null , TypeCombinator::containsNull ($ type )),
12431247 );
12441248
12451249 if ($ this ->hasAggregateWithoutGroupBy () && !$ expr instanceof AST \Functions \CountFunction) {
@@ -1997,7 +2001,7 @@ private function isQueryComponentNullable(string $dqlAlias): bool
19972001
19982002 /**
19992003 * @param ClassMetadata<object> $class
2000- * @return array{string, ?class-string<BackedEnum>} Doctrine type name and enum type of field
2004+ * @return array{string, ?class-string<BackedEnum>, ?list<string> } Doctrine type name, enum type of field, enum values
20012005 */
20022006 private function getTypeOfField (ClassMetadata $ class , string $ fieldName ): array
20032007 {
@@ -2015,11 +2019,45 @@ private function getTypeOfField(ClassMetadata $class, string $fieldName): array
20152019 $ enumType = null ;
20162020 }
20172021
2018- return [$ type , $ enumType ];
2022+ return [$ type , $ enumType, $ this -> detectEnumValues ( $ type , $ metadata ) ];
20192023 }
20202024
2021- /** @param ?class-string<BackedEnum> $enumType */
2022- private function resolveDoctrineType (string $ typeName , ?string $ enumType = null , bool $ nullable = false ): Type
2025+ /**
2026+ * @param mixed $metadata
2027+ *
2028+ * @return list<string>|null
2029+ */
2030+ private function detectEnumValues (string $ typeName , $ metadata ): ?array
2031+ {
2032+ if ($ typeName !== 'enum ' ) {
2033+ return null ;
2034+ }
2035+ 2036+ $ values = $ metadata ['options ' ]['values ' ] ?? [];
2037+ 2038+ if (!is_array ($ values ) || count ($ values ) === 0 ) {
2039+ return null ;
2040+ }
2041+ 2042+ foreach ($ values as $ value ) {
2043+ if (!is_string ($ value )) {
2044+ return null ;
2045+ }
2046+ }
2047+ 2048+ return array_values ($ values );
2049+ }
2050+ 2051+ /**
2052+ * @param ?class-string<BackedEnum> $enumType
2053+ * @param ?list<string> $enumValues
2054+ */
2055+ private function resolveDoctrineType (
2056+ string $ typeName ,
2057+ ?string $ enumType = null ,
2058+ ?array $ enumValues = null ,
2059+ bool $ nullable = false
2060+ ): Type
20232061 {
20242062 try {
20252063 $ type = $ this ->descriptorRegistry
@@ -2036,8 +2074,14 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null,
20362074 ), ...TypeUtils::getAccessoryTypes ($ type ));
20372075 }
20382076 }
2077+ 2078+ if ($ enumValues !== null ) {
2079+ $ enumValuesType = TypeCombinator::union (...array_map (static fn (string $ value ) => new ConstantStringType ($ value ), $ enumValues ));
2080+ $ type = TypeCombinator::intersect ($ enumValuesType , $ type );
2081+ }
2082+ 20392083 if ($ type instanceof NeverType) {
2040- $ type = new MixedType ();
2084+ $ type = new MixedType ();
20412085 }
20422086 } catch (DescriptorNotRegisteredException $ e ) {
20432087 if ($ enumType !== null ) {
@@ -2051,11 +2095,19 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null,
20512095 $ type = TypeCombinator::addNull ($ type );
20522096 }
20532097
2054- return $ type ;
2098+ return $ type ;
20552099 }
20562100
2057- /** @param ?class-string<BackedEnum> $enumType */
2058- private function resolveDatabaseInternalType (string $ typeName , ?string $ enumType = null , bool $ nullable = false ): Type
2101+ /**
2102+ * @param ?class-string<BackedEnum> $enumType
2103+ * @param ?list<string> $enumValues
2104+ */
2105+ private function resolveDatabaseInternalType (
2106+ string $ typeName ,
2107+ ?string $ enumType = null ,
2108+ ?array $ enumValues = null ,
2109+ bool $ nullable = false
2110+ ): Type
20592111 {
20602112 try {
20612113 $ descriptor = $ this ->descriptorRegistry ->get ($ typeName );
@@ -2074,6 +2126,11 @@ private function resolveDatabaseInternalType(string $typeName, ?string $enumType
20742126 $ type = TypeCombinator::intersect ($ enumType , $ type );
20752127 }
20762128
2129+ if ($ enumValues !== null ) {
2130+ $ enumValuesType = TypeCombinator::union (...array_map (static fn (string $ value ) => new ConstantStringType ($ value ), $ enumValues ));
2131+ $ type = TypeCombinator::intersect ($ enumValuesType , $ type );
2132+ }
2133+ 20772134 if ($ nullable ) {
20782135 $ type = TypeCombinator::addNull ($ type );
20792136 }
0 commit comments