Skip to content

Commit adf1826

Browse files
authored
Add missing descriptors for SmallFloatType and EnumType
1 parent a1a9efb commit adf1826

File tree

10 files changed

+244
-17
lines changed

10 files changed

+244
-17
lines changed

extension.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,9 @@ services:
353353
-
354354
class: PHPStan\Type\Doctrine\Descriptors\DecimalType
355355
tags: [phpstan.doctrine.typeDescriptor]
356+
-
357+
class: PHPStan\Type\Doctrine\Descriptors\EnumType
358+
tags: [phpstan.doctrine.typeDescriptor]
356359
-
357360
class: PHPStan\Type\Doctrine\Descriptors\FloatType
358361
tags: [phpstan.doctrine.typeDescriptor]
@@ -374,6 +377,9 @@ services:
374377
-
375378
class: PHPStan\Type\Doctrine\Descriptors\SimpleArrayType
376379
tags: [phpstan.doctrine.typeDescriptor]
380+
-
381+
class: PHPStan\Type\Doctrine\Descriptors\SmallFloatType
382+
tags: [phpstan.doctrine.typeDescriptor]
377383
-
378384
class: PHPStan\Type\Doctrine\Descriptors\SmallIntType
379385
tags: [phpstan.doctrine.typeDescriptor]

phpstan-baseline-dbal-4.neon

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: '#^Class Doctrine\\DBAL\\Types\\EnumType not found\.$#'
5+
identifier: class.notFound
6+
count: 1
7+
path: src/Type/Doctrine/Descriptors/EnumType.php
8+
9+
-
10+
message: '#^Method PHPStan\\Type\\Doctrine\\Descriptors\\EnumType\:\:getType\(\) should return class\-string\<Doctrine\\DBAL\\Types\\Type\> but returns string\.$#'
11+
identifier: return.type
12+
count: 1
13+
path: src/Type/Doctrine/Descriptors/EnumType.php
14+
15+
-
16+
message: '#^Class Doctrine\\DBAL\\Types\\SmallFloatType not found\.$#'
17+
identifier: class.notFound
18+
count: 1
19+
path: src/Type/Doctrine/Descriptors/SmallFloatType.php
20+
21+
-
22+
message: '#^Method PHPStan\\Type\\Doctrine\\Descriptors\\SmallFloatType\:\:getType\(\) should return class\-string\<Doctrine\\DBAL\\Types\\Type\> but returns string\.$#'
23+
identifier: return.type
24+
count: 1
25+
path: src/Type/Doctrine/Descriptors/SmallFloatType.php
26+
27+
-
28+
message: '#^Class Doctrine\\DBAL\\Types\\EnumType not found\.$#'
29+
identifier: class.notFound
30+
count: 1
31+
path: src/Type/Doctrine/Query/QueryResultTypeWalker.php
32+
33+

phpstan-baseline.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,9 @@ parameters:
317317
identifier: new.deprecatedClass
318318
count: 1
319319
path: tests/Type/Doctrine/DBAL/pdo.php
320+
321+
-
322+
message: '#^Parameter references internal interface Doctrine\\ORM\\Query\\AST\\Phase2OptimizableConditional in its type\.$#'
323+
identifier: parameter.internalInterface
324+
count: 2
325+
path: src/Type/Doctrine/Query/QueryResultTypeWalker.php

phpstan.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ includes:
44
- phpstan-baseline.neon
55
- phpstan-baseline-deprecations.neon
66
- phpstan-baseline-dbal-3.neon
7+
- phpstan-baseline-dbal-4.neon
78
- compatibility/orm-3-baseline.php
89
- vendor/phpstan/phpstan-strict-rules/rules.neon
910
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Descriptors;
4+
5+
use PHPStan\Type\StringType;
6+
use PHPStan\Type\Type;
7+
8+
class EnumType implements DoctrineTypeDescriptor
9+
{
10+
11+
public function getType(): string
12+
{
13+
return \Doctrine\DBAL\Types\EnumType::class;
14+
}
15+
16+
public function getWritableToPropertyType(): Type
17+
{
18+
return new StringType();
19+
}
20+
21+
public function getWritableToDatabaseType(): Type
22+
{
23+
return new StringType();
24+
}
25+
26+
public function getDatabaseInternalType(): Type
27+
{
28+
return new StringType();
29+
}
30+
31+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Descriptors;
4+
5+
class SmallFloatType extends FloatType
6+
{
7+
8+
public function getType(): string
9+
{
10+
return \Doctrine\DBAL\Types\SmallFloatType::class;
11+
}
12+
13+
}

src/Type/Doctrine/Query/QueryResultTypeWalker.php

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Type\Doctrine\Query;
44

55
use BackedEnum;
6+
use Doctrine\DBAL\Types\EnumType as DbalEnumType;
67
use Doctrine\DBAL\Types\StringType as DbalStringType;
78
use Doctrine\DBAL\Types\Type as DbalType;
89
use Doctrine\ORM\EntityManagerInterface;
@@ -26,6 +27,7 @@
2627
use PHPStan\Type\Constant\ConstantBooleanType;
2728
use PHPStan\Type\Constant\ConstantFloatType;
2829
use PHPStan\Type\Constant\ConstantIntegerType;
30+
use PHPStan\Type\Constant\ConstantStringType;
2931
use PHPStan\Type\ConstantTypeHelper;
3032
use PHPStan\Type\Doctrine\DescriptorNotRegisteredException;
3133
use PHPStan\Type\Doctrine\DescriptorRegistry;
@@ -53,6 +55,7 @@
5355
use function get_class;
5456
use function gettype;
5557
use function in_array;
58+
use function is_array;
5659
use function is_int;
5760
use function is_numeric;
5861
use function is_object;
@@ -286,13 +289,13 @@ public function walkPathExpression($pathExpr): string
286289

287290
switch ($pathExpr->type) {
288291
case AST\PathExpression::TYPE_STATE_FIELD:
289-
[$typeName, $enumType] = $this->getTypeOfField($class, $fieldName);
292+
[$typeName, $enumType, $enumValues] = $this->getTypeOfField($class, $fieldName);
290293

291294
$nullable = $this->isQueryComponentNullable($dqlAlias)
292295
|| $class->isNullable($fieldName)
293296
|| $this->hasAggregateWithoutGroupBy();
294297

295-
$fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $nullable);
298+
$fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $enumValues, $nullable);
296299

297300
return $this->marshalType($fieldType);
298301

@@ -326,12 +329,12 @@ public function walkPathExpression($pathExpr): string
326329
}
327330

328331
$targetFieldName = $identifierFieldNames[0];
329-
[$typeName, $enumType] = $this->getTypeOfField($targetClass, $targetFieldName);
332+
[$typeName, $enumType, $enumValues] = $this->getTypeOfField($targetClass, $targetFieldName);
330333

331334
$nullable = ($joinColumn['nullable'] ?? true)
332335
|| $this->hasAggregateWithoutGroupBy();
333336

334-
$fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $nullable);
337+
$fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $enumValues, $nullable);
335338

336339
return $this->marshalType($fieldType);
337340

@@ -685,7 +688,7 @@ public function walkFunction($function): string
685688
return $this->marshalType(new MixedType());
686689
}
687690

688-
[$typeName, $enumType] = $this->getTypeOfField($targetClass, $targetFieldName);
691+
[$typeName, $enumType, $enumValues] = $this->getTypeOfField($targetClass, $targetFieldName);
689692

690693
if (!isset($assoc['joinColumns'])) {
691694
return $this->marshalType(new MixedType());
@@ -708,7 +711,7 @@ public function walkFunction($function): string
708711
|| $this->isQueryComponentNullable($dqlAlias)
709712
|| $this->hasAggregateWithoutGroupBy();
710713

711-
$fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $nullable);
714+
$fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $enumValues, $nullable);
712715

713716
return $this->marshalType($fieldType);
714717

@@ -1206,13 +1209,13 @@ public function walkSelectExpression($selectExpression): string
12061209
assert(array_key_exists('metadata', $qComp));
12071210
$class = $qComp['metadata'];
12081211

1209-
[$typeName, $enumType] = $this->getTypeOfField($class, $fieldName);
1212+
[$typeName, $enumType, $enumValues] = $this->getTypeOfField($class, $fieldName);
12101213

12111214
$nullable = $this->isQueryComponentNullable($dqlAlias)
12121215
|| $class->isNullable($fieldName)
12131216
|| $this->hasAggregateWithoutGroupBy();
12141217

1215-
$type = $this->resolveDoctrineType($typeName, $enumType, $nullable);
1218+
$type = $this->resolveDoctrineType($typeName, $enumType, $enumValues, $nullable);
12161219

12171220
$this->typeBuilder->addScalar($resultAlias, $type);
12181221

@@ -1235,11 +1238,12 @@ public function walkSelectExpression($selectExpression): string
12351238
if (
12361239
$expr instanceof TypedExpression
12371240
&& !$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
12381242
) {
12391243
$dbalTypeName = DbalType::getTypeRegistry()->lookupName($expr->getReturnType());
12401244
$type = TypeCombinator::intersect( // e.g. count is typed as int, but we infer int<0, max>
12411245
$type,
1242-
$this->resolveDoctrineType($dbalTypeName, null, TypeCombinator::containsNull($type)),
1246+
$this->resolveDoctrineType($dbalTypeName, null, null, TypeCombinator::containsNull($type)),
12431247
);
12441248

12451249
if ($this->hasAggregateWithoutGroupBy() && !$expr instanceof AST\Functions\CountFunction) {
@@ -1997,7 +2001,7 @@ private function isQueryComponentNullable(string $dqlAlias): bool
19972001

19982002
/**
19992003
* @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
20012005
*/
20022006
private function getTypeOfField(ClassMetadata $class, string $fieldName): array
20032007
{
@@ -2015,11 +2019,45 @@ private function getTypeOfField(ClassMetadata $class, string $fieldName): array
20152019
$enumType = null;
20162020
}
20172021

2018-
return [$type, $enumType];
2022+
return [$type, $enumType, $this->detectEnumValues($type, $metadata)];
20192023
}
20202024

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
20232061
{
20242062
try {
20252063
$type = $this->descriptorRegistry
@@ -2036,8 +2074,14 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null,
20362074
), ...TypeUtils::getAccessoryTypes($type));
20372075
}
20382076
}
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+
20392083
if ($type instanceof NeverType) {
2040-
$type = new MixedType();
2084+
$type = new MixedType();
20412085
}
20422086
} catch (DescriptorNotRegisteredException $e) {
20432087
if ($enumType !== null) {
@@ -2051,11 +2095,19 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null,
20512095
$type = TypeCombinator::addNull($type);
20522096
}
20532097

2054-
return $type;
2098+
return $type;
20552099
}
20562100

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
20592111
{
20602112
try {
20612113
$descriptor = $this->descriptorRegistry->get($typeName);
@@ -2074,6 +2126,11 @@ private function resolveDatabaseInternalType(string $typeName, ?string $enumType
20742126
$type = TypeCombinator::intersect($enumType, $type);
20752127
}
20762128

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+
20772134
if ($nullable) {
20782135
$type = TypeCombinator::addNull($type);
20792136
}

tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use PHPStan\Type\StringType;
3636
use PHPStan\Type\Type;
3737
use PHPStan\Type\TypeCombinator;
38+
use PHPStan\Type\UnionType;
3839
use PHPStan\Type\VerbosityLevel;
3940
use QueryResult\Entities\Embedded;
4041
use QueryResult\Entities\JoinedChild;
@@ -44,6 +45,7 @@
4445
use QueryResult\Entities\One;
4546
use QueryResult\Entities\OneId;
4647
use QueryResult\Entities\SingleTableChild;
48+
use QueryResult\EntitiesDbal42\Dbal4Entity;
4749
use QueryResult\EntitiesEnum\EntityWithEnum;
4850
use QueryResult\EntitiesEnum\IntEnum;
4951
use QueryResult\EntitiesEnum\StringEnum;
@@ -187,6 +189,15 @@ public static function setUpBeforeClass(): void
187189
$em->persist($entityWithEnum);
188190
}
189191

192+
if (InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=4.2')) {
193+
assert(class_exists(Dbal4Entity::class));
194+
195+
$dbal4Entity = new Dbal4Entity();
196+
$dbal4Entity->enum = 'a';
197+
$dbal4Entity->smallfloat = 1.1;
198+
$em->persist($dbal4Entity);
199+
}
200+
190201
$em->flush();
191202
}
192203

@@ -1532,6 +1543,29 @@ private function yieldConditionalDataset(): iterable
15321543
];
15331544
}
15341545

1546+
if (InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=4.2')) {
1547+
yield 'enum and smallfloat' => [
1548+
$this->constantArray([
1549+
[
1550+
new ConstantStringType('enum'),
1551+
new UnionType([
1552+
new ConstantStringType('a'),
1553+
new ConstantStringType('b'),
1554+
new ConstantStringType('c'),
1555+
]),
1556+
],
1557+
[
1558+
new ConstantStringType('smallfloat'),
1559+
new FloatType(),
1560+
],
1561+
]),
1562+
'
1563+
SELECT e.enum, e.smallfloat
1564+
FROM QueryResult\EntitiesDbal42\Dbal4Entity e
1565+
',
1566+
];
1567+
}
1568+
15351569
$ormVersion = InstalledVersions::getVersion('doctrine/orm');
15361570
$hasOrm3 = $ormVersion !== null && strpos($ormVersion, '3.') === 0;
15371571

0 commit comments

Comments
 (0)