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