From 9bed562ef8dac756c433a576e1f86efbdf023eca Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Fri, 23 Aug 2024 11:47:37 +0200 Subject: [PATCH] Fix support for `enumType` on array fields --- src/Rules/Doctrine/ORM/EntityColumnRule.php | 66 ++++++++++++++----- .../Doctrine/Query/QueryResultTypeWalker.php | 35 ++++++---- .../Doctrine/ORM/EntityColumnRuleTest.php | 18 ++++- .../ORM/data-attributes/enum-type.php | 21 ++++++ .../Query/QueryResultTypeWalkerTest.php | 6 +- .../EntitiesEnum/EntityWithEnum.php | 6 ++ 6 files changed, 121 insertions(+), 31 deletions(-) diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index e51e9c95..70c1fcea 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -21,6 +21,7 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use Throwable; use function get_class; @@ -115,25 +116,58 @@ public function processNode(Node $node, Scope $scope): array $enumTypeString = $fieldMapping['enumType'] ?? null; if ($enumTypeString !== null) { - if ($this->reflectionProvider->hasClass($enumTypeString)) { - $enumReflection = $this->reflectionProvider->getClass($enumTypeString); - $backedEnumType = $enumReflection->getBackedEnumType(); - if ($backedEnumType !== null) { - if (!$backedEnumType->equals($writableToDatabaseType) || !$backedEnumType->equals($writableToPropertyType)) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match database type %s.', - $className, - $propertyName, - $backedEnumType->describe(VerbosityLevel::typeOnly()), - $enumReflection->getDisplayName(), - $writableToDatabaseType->describe(VerbosityLevel::typeOnly()) - ))->identifier('doctrine.enumType')->build(); + if ($writableToDatabaseType->isArray()->no() && $writableToPropertyType->isArray()->no()) { + if ($this->reflectionProvider->hasClass($enumTypeString)) { + $enumReflection = $this->reflectionProvider->getClass($enumTypeString); + $backedEnumType = $enumReflection->getBackedEnumType(); + if ($backedEnumType !== null) { + if (!$backedEnumType->equals($writableToDatabaseType) || !$backedEnumType->equals($writableToPropertyType)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match database type %s.', + $className, + $propertyName, + $backedEnumType->describe(VerbosityLevel::typeOnly()), + $enumReflection->getDisplayName(), + $writableToDatabaseType->describe(VerbosityLevel::typeOnly()) + ))->identifier('doctrine.enumType')->build(); + } } } + $enumType = new ObjectType($enumTypeString); + $writableToPropertyType = $enumType; + $writableToDatabaseType = $enumType; + } else { + $enumType = new ObjectType($enumTypeString); + if ($this->reflectionProvider->hasClass($enumTypeString)) { + $enumReflection = $this->reflectionProvider->getClass($enumTypeString); + $backedEnumType = $enumReflection->getBackedEnumType(); + if ($backedEnumType !== null) { + if (!$backedEnumType->equals($writableToDatabaseType->getIterableValueType()) || !$backedEnumType->equals($writableToPropertyType->getIterableValueType())) { + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match value type %s of the database type %s.', + $className, + $propertyName, + $backedEnumType->describe(VerbosityLevel::typeOnly()), + $enumReflection->getDisplayName(), + $writableToDatabaseType->getIterableValueType()->describe(VerbosityLevel::typeOnly()), + $writableToDatabaseType->describe(VerbosityLevel::typeOnly()) + ) + )->identifier('doctrine.enumType')->build(); + } + } + } + + $writableToPropertyType = TypeCombinator::intersect(new ArrayType( + $writableToPropertyType->getIterableKeyType(), + $enumType + ), ...TypeUtils::getAccessoryTypes($writableToPropertyType)); + $writableToDatabaseType = TypeCombinator::intersect(new ArrayType( + $writableToDatabaseType->getIterableKeyType(), + $enumType + ), ...TypeUtils::getAccessoryTypes($writableToDatabaseType)); + } - $enumType = new ObjectType($enumTypeString); - $writableToPropertyType = $enumType; - $writableToDatabaseType = $enumType; } $identifiers = []; diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 2ce3e1ce..4ef105d9 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -19,6 +19,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -39,6 +40,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use function array_key_exists; use function array_map; @@ -2009,17 +2011,28 @@ private function getTypeOfField(ClassMetadata $class, string $fieldName): array /** @param ?class-string $enumType */ private function resolveDoctrineType(string $typeName, ?string $enumType = null, bool $nullable = false): Type { - if ($enumType !== null) { - $type = new ObjectType($enumType); - } else { - try { - $type = $this->descriptorRegistry - ->get($typeName) - ->getWritableToPropertyType(); - if ($type instanceof NeverType) { - $type = new MixedType(); + try { + $type = $this->descriptorRegistry + ->get($typeName) + ->getWritableToPropertyType(); + + if ($enumType !== null) { + if ($type->isArray()->no()) { + $type = new ObjectType($enumType); + } else { + $type = TypeCombinator::intersect(new ArrayType( + $type->getIterableKeyType(), + new ObjectType($enumType) + ), ...TypeUtils::getAccessoryTypes($type)); } - } catch (DescriptorNotRegisteredException $e) { + } + if ($type instanceof NeverType) { + $type = new MixedType(); + } + } catch (DescriptorNotRegisteredException $e) { + if ($enumType !== null) { + $type = new ObjectType($enumType); + } else { $type = new MixedType(); } } @@ -2028,7 +2041,7 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null, $type = TypeCombinator::addNull($type); } - return $type; + return $type; } /** @param ?class-string $enumType */ diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 5dfcc639..c7de7957 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -391,15 +391,27 @@ public function testEnumType(?string $objectManagerLoader): void $this->analyse([__DIR__ . '/data-attributes/enum-type.php'], [ [ '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.', - 35, + 42, ], [ '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.', - 35, + 42, ], [ '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.', - 38, + 45, + ], + [ + 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type5 type mapping mismatch: database can contain array but property expects PHPStan\Rules\Doctrine\ORMAttributes\FooEnum.', + 51, + ], + [ + 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type5 type mapping mismatch: property can contain PHPStan\Rules\Doctrine\ORMAttributes\FooEnum but database expects array.', + 51, + ], + [ + '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.', + 63, ], ]); } diff --git a/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php b/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php index a0c3b937..50e0a2ec 100644 --- a/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php +++ b/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php @@ -18,6 +18,13 @@ enum BarEnum: string { } +enum BazEnum: int { + + case ONE = 1; + case TWO = 2; + +} + #[ORM\Entity] class Foo { @@ -40,4 +47,18 @@ class Foo #[ORM\Column] public FooEnum $type4; + #[ORM\Column(type: "simple_array", enumType: FooEnum::class)] + public FooEnum $type5; + + /** + * @var list + */ + #[ORM\Column(type: "simple_array", enumType: FooEnum::class)] + public array $type6; + + /** + * @var list + */ + #[ORM\Column(type: "simple_array", enumType: BazEnum::class)] + public array $type7; } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 003a0737..fd6fee74 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -14,7 +14,9 @@ use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Php\PhpVersion; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -181,6 +183,7 @@ public static function setUpBeforeClass(): void $entityWithEnum->stringEnumColumn = StringEnum::A; $entityWithEnum->intEnumColumn = IntEnum::A; $entityWithEnum->intEnumOnStringColumn = IntEnum::A; + $entityWithEnum->stringEnumListColumn = [StringEnum::A, StringEnum::B]; $em->persist($entityWithEnum); } @@ -1499,9 +1502,10 @@ private function yieldConditionalDataset(): iterable $this->constantArray([ [new ConstantStringType('stringEnumColumn'), new ObjectType(StringEnum::class)], [new ConstantStringType('intEnumColumn'), new ObjectType(IntEnum::class)], + [new ConstantStringType('stringEnumListColumn'), AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new ObjectType(StringEnum::class)))], ]), ' - SELECT e.stringEnumColumn, e.intEnumColumn + SELECT e.stringEnumColumn, e.intEnumColumn, e.stringEnumListColumn FROM QueryResult\EntitiesEnum\EntityWithEnum e ', ]; diff --git a/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php b/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php index c22acd45..2a4c535a 100644 --- a/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php +++ b/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php @@ -38,4 +38,10 @@ class EntityWithEnum * @Column(type="string", enumType="QueryResult\EntitiesEnum\IntEnum") */ public $intEnumOnStringColumn; + + /** + * @var list + * @Column(type="simple_array", enumType="QueryResult\EntitiesEnum\StringEnum") + */ + public $stringEnumListColumn; }