Skip to content

Commit 4d17bed

Browse files
stofondrejmirtes
authored andcommitted
Fix support for enumType on array fields
1 parent a9bb990 commit 4d17bed

File tree

6 files changed

+121
-31
lines changed

6 files changed

+121
-31
lines changed

Diff for: src/Rules/Doctrine/ORM/EntityColumnRule.php

+50-16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use PHPStan\Type\TypeCombinator;
2222
use PHPStan\Type\TypehintHelper;
2323
use PHPStan\Type\TypeTraverser;
24+
use PHPStan\Type\TypeUtils;
2425
use PHPStan\Type\VerbosityLevel;
2526
use Throwable;
2627
use function get_class;
@@ -115,25 +116,58 @@ public function processNode(Node $node, Scope $scope): array
115116

116117
$enumTypeString = $fieldMapping['enumType'] ?? null;
117118
if ($enumTypeString !== null) {
118-
if ($this->reflectionProvider->hasClass($enumTypeString)) {
119-
$enumReflection = $this->reflectionProvider->getClass($enumTypeString);
120-
$backedEnumType = $enumReflection->getBackedEnumType();
121-
if ($backedEnumType !== null) {
122-
if (!$backedEnumType->equals($writableToDatabaseType) || !$backedEnumType->equals($writableToPropertyType)) {
123-
$errors[] = RuleErrorBuilder::message(sprintf(
124-
'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match database type %s.',
125-
$className,
126-
$propertyName,
127-
$backedEnumType->describe(VerbosityLevel::typeOnly()),
128-
$enumReflection->getDisplayName(),
129-
$writableToDatabaseType->describe(VerbosityLevel::typeOnly())
130-
))->identifier('doctrine.enumType')->build();
119+
if ($writableToDatabaseType->isArray()->no() && $writableToPropertyType->isArray()->no()) {
120+
if ($this->reflectionProvider->hasClass($enumTypeString)) {
121+
$enumReflection = $this->reflectionProvider->getClass($enumTypeString);
122+
$backedEnumType = $enumReflection->getBackedEnumType();
123+
if ($backedEnumType !== null) {
124+
if (!$backedEnumType->equals($writableToDatabaseType) || !$backedEnumType->equals($writableToPropertyType)) {
125+
$errors[] = RuleErrorBuilder::message(sprintf(
126+
'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match database type %s.',
127+
$className,
128+
$propertyName,
129+
$backedEnumType->describe(VerbosityLevel::typeOnly()),
130+
$enumReflection->getDisplayName(),
131+
$writableToDatabaseType->describe(VerbosityLevel::typeOnly())
132+
))->identifier('doctrine.enumType')->build();
133+
}
131134
}
132135
}
136+
$enumType = new ObjectType($enumTypeString);
137+
$writableToPropertyType = $enumType;
138+
$writableToDatabaseType = $enumType;
139+
} else {
140+
$enumType = new ObjectType($enumTypeString);
141+
if ($this->reflectionProvider->hasClass($enumTypeString)) {
142+
$enumReflection = $this->reflectionProvider->getClass($enumTypeString);
143+
$backedEnumType = $enumReflection->getBackedEnumType();
144+
if ($backedEnumType !== null) {
145+
if (!$backedEnumType->equals($writableToDatabaseType->getIterableValueType()) || !$backedEnumType->equals($writableToPropertyType->getIterableValueType())) {
146+
$errors[] = RuleErrorBuilder::message(
147+
sprintf(
148+
'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match value type %s of the database type %s.',
149+
$className,
150+
$propertyName,
151+
$backedEnumType->describe(VerbosityLevel::typeOnly()),
152+
$enumReflection->getDisplayName(),
153+
$writableToDatabaseType->getIterableValueType()->describe(VerbosityLevel::typeOnly()),
154+
$writableToDatabaseType->describe(VerbosityLevel::typeOnly())
155+
)
156+
)->identifier('doctrine.enumType')->build();
157+
}
158+
}
159+
}
160+
161+
$writableToPropertyType = TypeCombinator::intersect(new ArrayType(
162+
$writableToPropertyType->getIterableKeyType(),
163+
$enumType
164+
), ...TypeUtils::getAccessoryTypes($writableToPropertyType));
165+
$writableToDatabaseType = TypeCombinator::intersect(new ArrayType(
166+
$writableToDatabaseType->getIterableKeyType(),
167+
$enumType
168+
), ...TypeUtils::getAccessoryTypes($writableToDatabaseType));
169+
133170
}
134-
$enumType = new ObjectType($enumTypeString);
135-
$writableToPropertyType = $enumType;
136-
$writableToDatabaseType = $enumType;
137171
}
138172

139173
$identifiers = [];

Diff for: src/Type/Doctrine/Query/QueryResultTypeWalker.php

+24-11
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPStan\ShouldNotHappenException;
2020
use PHPStan\TrinaryLogic;
2121
use PHPStan\Type\Accessory\AccessoryNumericStringType;
22+
use PHPStan\Type\ArrayType;
2223
use PHPStan\Type\BooleanType;
2324
use PHPStan\Type\Constant\ConstantBooleanType;
2425
use PHPStan\Type\Constant\ConstantFloatType;
@@ -39,6 +40,7 @@
3940
use PHPStan\Type\Type;
4041
use PHPStan\Type\TypeCombinator;
4142
use PHPStan\Type\TypeTraverser;
43+
use PHPStan\Type\TypeUtils;
4244
use PHPStan\Type\UnionType;
4345
use function array_key_exists;
4446
use function array_map;
@@ -2009,17 +2011,28 @@ private function getTypeOfField(ClassMetadata $class, string $fieldName): array
20092011
/** @param ?class-string<BackedEnum> $enumType */
20102012
private function resolveDoctrineType(string $typeName, ?string $enumType = null, bool $nullable = false): Type
20112013
{
2012-
if ($enumType !== null) {
2013-
$type = new ObjectType($enumType);
2014-
} else {
2015-
try {
2016-
$type = $this->descriptorRegistry
2017-
->get($typeName)
2018-
->getWritableToPropertyType();
2019-
if ($type instanceof NeverType) {
2020-
$type = new MixedType();
2014+
try {
2015+
$type = $this->descriptorRegistry
2016+
->get($typeName)
2017+
->getWritableToPropertyType();
2018+
2019+
if ($enumType !== null) {
2020+
if ($type->isArray()->no()) {
2021+
$type = new ObjectType($enumType);
2022+
} else {
2023+
$type = TypeCombinator::intersect(new ArrayType(
2024+
$type->getIterableKeyType(),
2025+
new ObjectType($enumType)
2026+
), ...TypeUtils::getAccessoryTypes($type));
20212027
}
2022-
} catch (DescriptorNotRegisteredException $e) {
2028+
}
2029+
if ($type instanceof NeverType) {
2030+
$type = new MixedType();
2031+
}
2032+
} catch (DescriptorNotRegisteredException $e) {
2033+
if ($enumType !== null) {
2034+
$type = new ObjectType($enumType);
2035+
} else {
20232036
$type = new MixedType();
20242037
}
20252038
}
@@ -2028,7 +2041,7 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null,
20282041
$type = TypeCombinator::addNull($type);
20292042
}
20302043

2031-
return $type;
2044+
return $type;
20322045
}
20332046

20342047
/** @param ?class-string<BackedEnum> $enumType */

Diff for: tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php

+15-3
Original file line numberDiff line numberDiff line change
@@ -391,15 +391,27 @@ public function testEnumType(?string $objectManagerLoader): void
391391
$this->analyse([__DIR__ . '/data-attributes/enum-type.php'], [
392392
[
393393
'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type2 type mapping mismatch: database can contain PHPStan\Rules\Doctrine\ORMAttributes\FooEnum but property expects PHPStan\Rules\Doctrine\ORMAttributes\BarEnum.',
394-
35,
394+
42,
395395
],
396396
[
397397
'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type2 type mapping mismatch: property can contain PHPStan\Rules\Doctrine\ORMAttributes\BarEnum but database expects PHPStan\Rules\Doctrine\ORMAttributes\FooEnum.',
398-
35,
398+
42,
399399
],
400400
[
401401
'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type3 type mapping mismatch: backing type string of enum PHPStan\Rules\Doctrine\ORMAttributes\FooEnum does not match database type int.',
402-
38,
402+
45,
403+
],
404+
[
405+
'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type5 type mapping mismatch: database can contain array<int, PHPStan\Rules\Doctrine\ORMAttributes\FooEnum> but property expects PHPStan\Rules\Doctrine\ORMAttributes\FooEnum.',
406+
51,
407+
],
408+
[
409+
'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type5 type mapping mismatch: property can contain PHPStan\Rules\Doctrine\ORMAttributes\FooEnum but database expects array<PHPStan\Rules\Doctrine\ORMAttributes\FooEnum>.',
410+
51,
411+
],
412+
[
413+
'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type7 type mapping mismatch: backing type int of enum PHPStan\Rules\Doctrine\ORMAttributes\BazEnum does not match value type string of the database type array<string>.',
414+
63,
403415
],
404416
]);
405417
}

Diff for: tests/Rules/Doctrine/ORM/data-attributes/enum-type.php

+21
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ enum BarEnum: string {
1818

1919
}
2020

21+
enum BazEnum: int {
22+
23+
case ONE = 1;
24+
case TWO = 2;
25+
26+
}
27+
2128
#[ORM\Entity]
2229
class Foo
2330
{
@@ -40,4 +47,18 @@ class Foo
4047
#[ORM\Column]
4148
public FooEnum $type4;
4249

50+
#[ORM\Column(type: "simple_array", enumType: FooEnum::class)]
51+
public FooEnum $type5;
52+
53+
/**
54+
* @var list<FooEnum>
55+
*/
56+
#[ORM\Column(type: "simple_array", enumType: FooEnum::class)]
57+
public array $type6;
58+
59+
/**
60+
* @var list<BazEnum>
61+
*/
62+
#[ORM\Column(type: "simple_array", enumType: BazEnum::class)]
63+
public array $type7;
4364
}

Diff for: tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
use PHPStan\Doctrine\Driver\DriverDetector;
1515
use PHPStan\Php\PhpVersion;
1616
use PHPStan\Testing\PHPStanTestCase;
17+
use PHPStan\Type\Accessory\AccessoryArrayListType;
1718
use PHPStan\Type\Accessory\AccessoryNumericStringType;
19+
use PHPStan\Type\ArrayType;
1820
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1921
use PHPStan\Type\Constant\ConstantFloatType;
2022
use PHPStan\Type\Constant\ConstantIntegerType;
@@ -181,6 +183,7 @@ public static function setUpBeforeClass(): void
181183
$entityWithEnum->stringEnumColumn = StringEnum::A;
182184
$entityWithEnum->intEnumColumn = IntEnum::A;
183185
$entityWithEnum->intEnumOnStringColumn = IntEnum::A;
186+
$entityWithEnum->stringEnumListColumn = [StringEnum::A, StringEnum::B];
184187
$em->persist($entityWithEnum);
185188
}
186189

@@ -1499,9 +1502,10 @@ private function yieldConditionalDataset(): iterable
14991502
$this->constantArray([
15001503
[new ConstantStringType('stringEnumColumn'), new ObjectType(StringEnum::class)],
15011504
[new ConstantStringType('intEnumColumn'), new ObjectType(IntEnum::class)],
1505+
[new ConstantStringType('stringEnumListColumn'), AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new ObjectType(StringEnum::class)))],
15021506
]),
15031507
'
1504-
SELECT e.stringEnumColumn, e.intEnumColumn
1508+
SELECT e.stringEnumColumn, e.intEnumColumn, e.stringEnumListColumn
15051509
FROM QueryResult\EntitiesEnum\EntityWithEnum e
15061510
',
15071511
];

Diff for: tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php

+6
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,10 @@ class EntityWithEnum
3838
* @Column(type="string", enumType="QueryResult\EntitiesEnum\IntEnum")
3939
*/
4040
public $intEnumOnStringColumn;
41+
42+
/**
43+
* @var list<StringEnum>
44+
* @Column(type="simple_array", enumType="QueryResult\EntitiesEnum\StringEnum")
45+
*/
46+
public $stringEnumListColumn;
4147
}

0 commit comments

Comments
 (0)