27
27
use PHPStan \Type \Constant \ConstantBooleanType ;
28
28
use PHPStan \Type \Constant \ConstantFloatType ;
29
29
use PHPStan \Type \Constant \ConstantIntegerType ;
30
+ use PHPStan \Type \Constant \ConstantStringType ;
30
31
use PHPStan \Type \ConstantTypeHelper ;
31
32
use PHPStan \Type \Doctrine \DescriptorNotRegisteredException ;
32
33
use PHPStan \Type \Doctrine \DescriptorRegistry ;
54
55
use function get_class ;
55
56
use function gettype ;
56
57
use function in_array ;
58
+ use function is_array ;
57
59
use function is_int ;
58
60
use function is_numeric ;
59
61
use function is_object ;
@@ -287,13 +289,13 @@ public function walkPathExpression($pathExpr): string
287
289
288
290
switch ($ pathExpr ->type ) {
289
291
case AST \PathExpression::TYPE_STATE_FIELD :
290
- [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ class , $ fieldName );
292
+ [$ typeName , $ enumType, $ enumValues ] = $ this ->getTypeOfField ($ class , $ fieldName );
291
293
292
294
$ nullable = $ this ->isQueryComponentNullable ($ dqlAlias )
293
295
|| $ class ->isNullable ($ fieldName )
294
296
|| $ this ->hasAggregateWithoutGroupBy ();
295
297
296
- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ nullable );
298
+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ enumValues , $ nullable );
297
299
298
300
return $ this ->marshalType ($ fieldType );
299
301
@@ -327,12 +329,12 @@ public function walkPathExpression($pathExpr): string
327
329
}
328
330
329
331
$ targetFieldName = $ identifierFieldNames [0 ];
330
- [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
332
+ [$ typeName , $ enumType, $ enumValues ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
331
333
332
334
$ nullable = ($ joinColumn ['nullable ' ] ?? true )
333
335
|| $ this ->hasAggregateWithoutGroupBy ();
334
336
335
- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ nullable );
337
+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ enumValues , $ nullable );
336
338
337
339
return $ this ->marshalType ($ fieldType );
338
340
@@ -686,7 +688,7 @@ public function walkFunction($function): string
686
688
return $ this ->marshalType (new MixedType ());
687
689
}
688
690
689
- [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
691
+ [$ typeName , $ enumType, $ enumValues ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
690
692
691
693
if (!isset ($ assoc ['joinColumns ' ])) {
692
694
return $ this ->marshalType (new MixedType ());
@@ -709,7 +711,7 @@ public function walkFunction($function): string
709
711
|| $ this ->isQueryComponentNullable ($ dqlAlias )
710
712
|| $ this ->hasAggregateWithoutGroupBy ();
711
713
712
- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ nullable );
714
+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ enumValues , $ nullable );
713
715
714
716
return $ this ->marshalType ($ fieldType );
715
717
@@ -1207,13 +1209,13 @@ public function walkSelectExpression($selectExpression): string
1207
1209
assert (array_key_exists ('metadata ' , $ qComp ));
1208
1210
$ class = $ qComp ['metadata ' ];
1209
1211
1210
- [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ class , $ fieldName );
1212
+ [$ typeName , $ enumType, $ enumValues ] = $ this ->getTypeOfField ($ class , $ fieldName );
1211
1213
1212
1214
$ nullable = $ this ->isQueryComponentNullable ($ dqlAlias )
1213
1215
|| $ class ->isNullable ($ fieldName )
1214
1216
|| $ this ->hasAggregateWithoutGroupBy ();
1215
1217
1216
- $ type = $ this ->resolveDoctrineType ($ typeName , $ enumType , $ nullable );
1218
+ $ type = $ this ->resolveDoctrineType ($ typeName , $ enumType , $ enumValues , $ nullable );
1217
1219
1218
1220
$ this ->typeBuilder ->addScalar ($ resultAlias , $ type );
1219
1221
@@ -1241,7 +1243,7 @@ public function walkSelectExpression($selectExpression): string
1241
1243
$ dbalTypeName = DbalType::getTypeRegistry ()->lookupName ($ expr ->getReturnType ());
1242
1244
$ type = TypeCombinator::intersect ( // e.g. count is typed as int, but we infer int<0, max>
1243
1245
$ type ,
1244
- $ this ->resolveDoctrineType ($ dbalTypeName , null , TypeCombinator::containsNull ($ type )),
1246
+ $ this ->resolveDoctrineType ($ dbalTypeName , null , null , TypeCombinator::containsNull ($ type )),
1245
1247
);
1246
1248
1247
1249
if ($ this ->hasAggregateWithoutGroupBy () && !$ expr instanceof AST \Functions \CountFunction) {
@@ -1999,7 +2001,7 @@ private function isQueryComponentNullable(string $dqlAlias): bool
1999
2001
2000
2002
/**
2001
2003
* @param ClassMetadata<object> $class
2002
- * @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
2003
2005
*/
2004
2006
private function getTypeOfField (ClassMetadata $ class , string $ fieldName ): array
2005
2007
{
@@ -2017,11 +2019,45 @@ private function getTypeOfField(ClassMetadata $class, string $fieldName): array
2017
2019
$ enumType = null ;
2018
2020
}
2019
2021
2020
- return [$ type , $ enumType ];
2022
+ return [$ type , $ enumType, $ this -> detectEnumValues ( $ type , $ metadata ) ];
2021
2023
}
2022
2024
2023
- /** @param ?class-string<BackedEnum> $enumType */
2024
- 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
2025
2061
{
2026
2062
try {
2027
2063
$ type = $ this ->descriptorRegistry
@@ -2038,8 +2074,14 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null,
2038
2074
), ...TypeUtils::getAccessoryTypes ($ type ));
2039
2075
}
2040
2076
}
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
+
2041
2083
if ($ type instanceof NeverType) {
2042
- $ type = new MixedType ();
2084
+ $ type = new MixedType ();
2043
2085
}
2044
2086
} catch (DescriptorNotRegisteredException $ e ) {
2045
2087
if ($ enumType !== null ) {
@@ -2053,11 +2095,19 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null,
2053
2095
$ type = TypeCombinator::addNull ($ type );
2054
2096
}
2055
2097
2056
- return $ type ;
2098
+ return $ type ;
2057
2099
}
2058
2100
2059
- /** @param ?class-string<BackedEnum> $enumType */
2060
- 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
2061
2111
{
2062
2112
try {
2063
2113
$ descriptor = $ this ->descriptorRegistry ->get ($ typeName );
@@ -2076,6 +2126,11 @@ private function resolveDatabaseInternalType(string $typeName, ?string $enumType
2076
2126
$ type = TypeCombinator::intersect ($ enumType , $ type );
2077
2127
}
2078
2128
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
+
2079
2134
if ($ nullable ) {
2080
2135
$ type = TypeCombinator::addNull ($ type );
2081
2136
}
0 commit comments