Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 7ffe41d

Browse files
Enum support in query type inference
1 parent f855eba commit 7ffe41d

File tree

10 files changed

+1393
-1085
lines changed

10 files changed

+1393
-1085
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: 55 additions & 26 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, $enumType] = $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, $enumType, $nullable);
284285

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

@@ -530,16 +531,13 @@ public function walkFunction($function)
530531
$targetFieldName = $function->fieldMapping;
531532
}
532533

533-
$typeName = $targetClass->getTypeOfField($targetFieldName);
534-
if ($typeName === null) {
535-
return $this->marshalType(new MixedType());
536-
}
537-
538534
$fieldMapping = $targetClass->fieldMappings[$targetFieldName] ?? null;
539535
if ($fieldMapping === null) {
540536
return $this->marshalType(new MixedType());
541537
}
542538

539+
[$typeName, $enumType] = $this->getTypeOfField($targetClass, $targetFieldName);
540+
543541
$joinColumn = null;
544542

545543
foreach ($assoc['joinColumns'] as $item) {
@@ -556,7 +554,7 @@ public function walkFunction($function)
556554
$nullable = (bool) ($joinColumn['nullable'] ?? true)
557555
|| $this->hasAggregateWithoutGroupBy();
558556

559-
$fieldType = $this->resolveDatabaseInternalType($typeName, $nullable);
557+
$fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $nullable);
560558

561559
return $this->marshalType($fieldType);
562560

@@ -783,15 +781,13 @@ public function walkSelectExpression($selectExpression)
783781
$qComp = $this->queryComponents[$dqlAlias];
784782
$class = $qComp['metadata'];
785783

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

790786
$nullable = $this->isQueryComponentNullable($dqlAlias)
791787
|| $class->isNullable($fieldName)
792788
|| $this->hasAggregateWithoutGroupBy();
793789

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

796792
$this->typeBuilder->addScalar($resultAlias, $type);
797793

@@ -1295,14 +1291,37 @@ private function isQueryComponentNullable(string $dqlAlias): bool
12951291
return $this->nullableQueryComponents[$dqlAlias] ?? false;
12961292
}
12971293

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
12991296
{
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+
}
13061325
}
13071326

13081327
if ($nullable) {
@@ -1312,7 +1331,8 @@ private function resolveDoctrineType(string $typeName, bool $nullable = false):
13121331
return $type;
13131332
}
13141333

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
13161336
{
13171337
try {
13181338
$type = $this->descriptorRegistry
@@ -1322,6 +1342,15 @@ private function resolveDatabaseInternalType(string $typeName, bool $nullable =
13221342
$type = new MixedType();
13231343
}
13241344

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+
13251354
if ($nullable) {
13261355
$type = TypeCombinator::addNull($type);
13271356
}

‎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)

AltStyle によって変換されたページ (->オリジナル) /