Skip to content

Commit 31c033b

Browse files
committed
Enum support in query type inference
1 parent f855eba commit 31c033b

File tree

10 files changed

+1391
-1080
lines changed

10 files changed

+1391
-1080
lines changed

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"doctrine/persistence": "^1.3.8 || ^2.2.1",
2828
"nesbot/carbon": "^2.49",
2929
"nikic/php-parser": "^4.13.2",
30+
"ocramius/package-versions": "*",
3031
"php-parallel-lint/php-parallel-lint": "^1.2",
3132
"phpstan/phpstan-phpunit": "^1.0",
3233
"phpstan/phpstan-strict-rules": "^1.0",
@@ -41,7 +42,8 @@
4142
},
4243
"sort-packages": true,
4344
"allow-plugins": {
44-
"composer/package-versions-deprecated": true
45+
"composer/package-versions-deprecated": true,
46+
"ocramius/package-versions": true
4547
}
4648
},
4749
"extra": {

phpstan.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ parameters:
1717

1818
reportUnmatchedIgnoredErrors: false
1919

20+
bootstrapFiles:
21+
- stubs/runtime/Enum/UnitEnum.php
22+
- stubs/runtime/Enum/BackedEnum.php
23+
2024
ignoreErrors:
2125
-
2226
message: '~^Variable method call on Doctrine\\ORM\\QueryBuilder~'

src/Type/Doctrine/Query/QueryResultTypeWalker.php

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
namespace PHPStan\Type\Doctrine\Query;
44

5+
use BackedEnum;
56
use Doctrine\ORM\EntityManagerInterface;
67
use Doctrine\ORM\Mapping\ClassMetadata;
8+
use Doctrine\ORM\Mapping\ClassMetadataInfo;
79
use Doctrine\ORM\Query;
810
use Doctrine\ORM\Query\AST;
911
use Doctrine\ORM\Query\AST\TypedExpression;
@@ -15,6 +17,7 @@
1517
use PHPStan\Type\Constant\ConstantFloatType;
1618
use PHPStan\Type\Constant\ConstantIntegerType;
1719
use PHPStan\Type\Constant\ConstantStringType;
20+
use PHPStan\Type\ConstantTypeHelper;
1821
use PHPStan\Type\Doctrine\DescriptorNotRegisteredException;
1922
use PHPStan\Type\Doctrine\DescriptorRegistry;
2023
use PHPStan\Type\FloatType;
@@ -31,6 +34,7 @@
3134
use PHPStan\Type\TypeTraverser;
3235
use PHPStan\Type\TypeUtils;
3336
use PHPStan\Type\UnionType;
37+
use function array_map;
3438
use function assert;
3539
use function class_exists;
3640
use function count;
@@ -42,6 +46,7 @@
4246
use function is_numeric;
4347
use function is_object;
4448
use function is_string;
49+
use function is_subclass_of;
4550
use function serialize;
4651
use function sprintf;
4752
use 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] = $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, null, $nullable);
284285

285286
return $this->marshalType($fieldType);
286287

@@ -556,7 +557,7 @@ public function walkFunction($function)
556557
$nullable = (bool) ($joinColumn['nullable'] ?? true)
557558
|| $this->hasAggregateWithoutGroupBy();
558559

559-
$fieldType = $this->resolveDatabaseInternalType($typeName, $nullable);
560+
$fieldType = $this->resolveDatabaseInternalType($typeName, null, $nullable);
560561

561562
return $this->marshalType($fieldType);
562563

@@ -783,15 +784,13 @@ public function walkSelectExpression($selectExpression)
783784
$qComp = $this->queryComponents[$dqlAlias];
784785
$class = $qComp['metadata'];
785786

786-
$typeName = $class->getTypeOfField($fieldName);
787-
788-
assert(is_string($typeName));
787+
[$typeName, $enumType] = $this->getTypeOfField($class, $fieldName);
789788

790789
$nullable = $this->isQueryComponentNullable($dqlAlias)
791790
|| $class->isNullable($fieldName)
792791
|| $this->hasAggregateWithoutGroupBy();
793792

794-
$type = $this->resolveDoctrineType($typeName, $nullable);
793+
$type = $this->resolveDoctrineType($typeName, $enumType, $nullable);
795794

796795
$this->typeBuilder->addScalar($resultAlias, $type);
797796

@@ -1295,14 +1294,37 @@ private function isQueryComponentNullable(string $dqlAlias): bool
12951294
return $this->nullableQueryComponents[$dqlAlias] ?? false;
12961295
}
12971296

1298-
private function resolveDoctrineType(string $typeName, bool $nullable = false): Type
1297+
/** @return array{string, ?class-string<BackedEnum>} Doctrine type name and enum type of field */
1298+
private function getTypeOfField(ClassMetadataInfo $class, string $fieldName): array
12991299
{
1300-
try {
1301-
$type = $this->descriptorRegistry
1302-
->get($typeName)
1303-
->getWritableToPropertyType();
1304-
} catch (DescriptorNotRegisteredException $e) {
1305-
$type = new MixedType();
1300+
assert(isset($class->fieldMappings[$fieldName]));
1301+
1302+
/** @var array{type: string, enumType?: ?string} $metadata */
1303+
$metadata = $class->fieldMappings[$fieldName];
1304+
1305+
$type = $metadata['type'];
1306+
$enumType = $metadata['enumType'] ?? null;
1307+
1308+
if (!is_string($enumType) || !class_exists($enumType) || !is_subclass_of($enumType, BackedEnum::class)) {
1309+
$enumType = null;
1310+
}
1311+
1312+
return [$type, $enumType];
1313+
}
1314+
1315+
/** @param ?class-string<BackedEnum> $enumType */
1316+
private function resolveDoctrineType(string $typeName, ?string $enumType = null, bool $nullable = false): Type
1317+
{
1318+
if ($enumType !== null) {
1319+
$type = new ObjectType($enumType);
1320+
} else {
1321+
try {
1322+
$type = $this->descriptorRegistry
1323+
->get($typeName)
1324+
->getWritableToPropertyType();
1325+
} catch (DescriptorNotRegisteredException $e) {
1326+
$type = new MixedType();
1327+
}
13061328
}
13071329

13081330
if ($nullable) {
@@ -1312,7 +1334,8 @@ private function resolveDoctrineType(string $typeName, bool $nullable = false):
13121334
return $type;
13131335
}
13141336

1315-
private function resolveDatabaseInternalType(string $typeName, bool $nullable = false): Type
1337+
/** @param ?class-string<BackedEnum> $enumType */
1338+
private function resolveDatabaseInternalType(string $typeName, ?string $enumType = null, bool $nullable = false): Type
13161339
{
13171340
try {
13181341
$type = $this->descriptorRegistry
@@ -1322,6 +1345,15 @@ private function resolveDatabaseInternalType(string $typeName, bool $nullable =
13221345
$type = new MixedType();
13231346
}
13241347

1348+
if ($enumType !== null) {
1349+
$enumTypes = array_map(static function ($enumType) {
1350+
return ConstantTypeHelper::getTypeFromValue($enumType->value);
1351+
}, $enumType::cases());
1352+
$enumType = TypeCombinator::union(...$enumTypes);
1353+
$enumType = TypeCombinator::union($enumType, $enumType->toString());
1354+
$type = TypeCombinator::intersect($enumType, $type);
1355+
}
1356+
13251357
if ($nullable) {
13261358
$type = TypeCombinator::addNull($type);
13271359
}

stubs/runtime/Enum/BackedEnum.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
if (\PHP_VERSION_ID < 80100) {
4+
if (interface_exists('BackedEnum', false)) {
5+
return;
6+
}
7+
8+
interface BackedEnum extends UnitEnum
9+
{
10+
/**
11+
* @param int|string $value
12+
* @return static
13+
*/
14+
public static function from($value);
15+
16+
/**
17+
* @param int|string $value
18+
* @return ?static
19+
*/
20+
public static function tryFrom($value);
21+
}
22+
}

stubs/runtime/Enum/UnitEnum.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
if (\PHP_VERSION_ID < 80100) {
4+
if (interface_exists('UnitEnum', false)) {
5+
return;
6+
}
7+
8+
interface UnitEnum
9+
{
10+
/**
11+
* @return static[]
12+
*/
13+
public static function cases(): array;
14+
}
15+
}

0 commit comments

Comments
 (0)