2
2
3
3
namespace PHPStan \Type \Doctrine \Query ;
4
4
5
+ use BackedEnum ;
5
6
use Doctrine \ORM \EntityManagerInterface ;
6
7
use Doctrine \ORM \Mapping \ClassMetadata ;
8
+ use Doctrine \ORM \Mapping \ClassMetadataInfo ;
7
9
use Doctrine \ORM \Query ;
8
10
use Doctrine \ORM \Query \AST ;
9
11
use Doctrine \ORM \Query \AST \TypedExpression ;
15
17
use PHPStan \Type \Constant \ConstantFloatType ;
16
18
use PHPStan \Type \Constant \ConstantIntegerType ;
17
19
use PHPStan \Type \Constant \ConstantStringType ;
20
+ use PHPStan \Type \ConstantTypeHelper ;
18
21
use PHPStan \Type \Doctrine \DescriptorNotRegisteredException ;
19
22
use PHPStan \Type \Doctrine \DescriptorRegistry ;
20
23
use PHPStan \Type \FloatType ;
31
34
use PHPStan \Type \TypeTraverser ;
32
35
use PHPStan \Type \TypeUtils ;
33
36
use PHPStan \Type \UnionType ;
37
+ use function array_map ;
34
38
use function assert ;
35
39
use function class_exists ;
36
40
use function count ;
42
46
use function is_numeric ;
43
47
use function is_object ;
44
48
use function is_string ;
49
+ use function is_subclass_of ;
45
50
use function serialize ;
46
51
use function sprintf ;
47
52
use function strtolower ;
@@ -231,15 +236,13 @@ public function walkPathExpression($pathExpr)
231
236
232
237
switch ($ pathExpr ->type ) {
233
238
case AST \PathExpression::TYPE_STATE_FIELD :
234
- $ typeName = $ class ->getTypeOfField ($ fieldName );
235
-
236
- assert (is_string ($ typeName ));
239
+ [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ class , $ fieldName );
237
240
238
241
$ nullable = $ this ->isQueryComponentNullable ($ dqlAlias )
239
242
|| $ class ->isNullable ($ fieldName )
240
243
|| $ this ->hasAggregateWithoutGroupBy ();
241
244
242
- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ nullable );
245
+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ nullable );
243
246
244
247
return $ this ->marshalType ($ fieldType );
245
248
@@ -273,14 +276,12 @@ public function walkPathExpression($pathExpr)
273
276
}
274
277
275
278
$ targetFieldName = $ identifierFieldNames [0 ];
276
- $ typeName = $ targetClass ->getTypeOfField ($ targetFieldName );
277
-
278
- assert (is_string ($ typeName ));
279
+ [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
279
280
280
281
$ nullable = (bool ) ($ joinColumn ['nullable ' ] ?? true )
281
282
|| $ this ->hasAggregateWithoutGroupBy ();
282
283
283
- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ nullable );
284
+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ nullable );
284
285
285
286
return $ this ->marshalType ($ fieldType );
286
287
@@ -530,16 +531,13 @@ public function walkFunction($function)
530
531
$ targetFieldName = $ function ->fieldMapping ;
531
532
}
532
533
533
- $ typeName = $ targetClass ->getTypeOfField ($ targetFieldName );
534
- if ($ typeName === null ) {
535
- return $ this ->marshalType (new MixedType ());
536
- }
537
-
538
534
$ fieldMapping = $ targetClass ->fieldMappings [$ targetFieldName ] ?? null ;
539
535
if ($ fieldMapping === null ) {
540
536
return $ this ->marshalType (new MixedType ());
541
537
}
542
538
539
+ [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ targetClass , $ targetFieldName );
540
+
543
541
$ joinColumn = null ;
544
542
545
543
foreach ($ assoc ['joinColumns ' ] as $ item ) {
@@ -556,7 +554,7 @@ public function walkFunction($function)
556
554
$ nullable = (bool ) ($ joinColumn ['nullable ' ] ?? true )
557
555
|| $ this ->hasAggregateWithoutGroupBy ();
558
556
559
- $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ nullable );
557
+ $ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ enumType , $ nullable );
560
558
561
559
return $ this ->marshalType ($ fieldType );
562
560
@@ -783,15 +781,13 @@ public function walkSelectExpression($selectExpression)
783
781
$ qComp = $ this ->queryComponents [$ dqlAlias ];
784
782
$ class = $ qComp ['metadata ' ];
785
783
786
- $ typeName = $ class ->getTypeOfField ($ fieldName );
787
-
788
- assert (is_string ($ typeName ));
784
+ [$ typeName , $ enumType ] = $ this ->getTypeOfField ($ class , $ fieldName );
789
785
790
786
$ nullable = $ this ->isQueryComponentNullable ($ dqlAlias )
791
787
|| $ class ->isNullable ($ fieldName )
792
788
|| $ this ->hasAggregateWithoutGroupBy ();
793
789
794
- $ type = $ this ->resolveDoctrineType ($ typeName , $ nullable );
790
+ $ type = $ this ->resolveDoctrineType ($ typeName , $ enumType , $ nullable );
795
791
796
792
$ this ->typeBuilder ->addScalar ($ resultAlias , $ type );
797
793
@@ -1295,14 +1291,37 @@ private function isQueryComponentNullable(string $dqlAlias): bool
1295
1291
return $ this ->nullableQueryComponents [$ dqlAlias ] ?? false ;
1296
1292
}
1297
1293
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
1299
1296
{
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
+ }
1306
1325
}
1307
1326
1308
1327
if ($ nullable ) {
@@ -1312,7 +1331,8 @@ private function resolveDoctrineType(string $typeName, bool $nullable = false):
1312
1331
return $ type ;
1313
1332
}
1314
1333
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
1316
1336
{
1317
1337
try {
1318
1338
$ type = $ this ->descriptorRegistry
@@ -1322,6 +1342,15 @@ private function resolveDatabaseInternalType(string $typeName, bool $nullable =
1322
1342
$ type = new MixedType ();
1323
1343
}
1324
1344
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
+
1325
1354
if ($ nullable ) {
1326
1355
$ type = TypeCombinator::addNull ($ type );
1327
1356
}
0 commit comments