From af955bb150d477ab0c17a070fc9c88eaa28f03a2 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 25 Jun 2024 10:17:21 +0200 Subject: [PATCH 1/2] ReflectionDescriptor: deduce database internal type based on parent --- README.md | 4 +++ .../Doctrine/DefaultDescriptorRegistry.php | 11 +++++++ .../Descriptors/ReflectionDescriptor.php | 32 +++++++++++++++++-- .../Doctrine/ORM/EntityColumnRuleTest.php | 8 ++--- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a93658c5..9ff192b1 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,10 @@ Type descriptors don't have to deal with nullable types, as these are transparen If your custom type's `convertToPHPValue()` and `convertToDatabaseValue()` methods have proper typehints, you don't have to write your own descriptor for it. The `PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor` can analyse the typehints and do the rest for you. +If parent of your type is one of the Doctrine's non-abstract ones, `ReflectionDescriptor` will reuse its descriptor even for expression resolution (e.g. `AVG(t.cost)`). +For example, if you extend `Doctrine\DBAL\Types\DecimalType`, it will know that sqlite fetches that as `float|int` and other drivers as `numeric-string`. +If you extend only `Doctrine\DBAL\Types\Type`, you should use custom descriptor and optionally implement even `DoctrineTypeDriverAwareDescriptor` to provide driver-specific resolution. + ### Registering type descriptors When you write a custom type descriptor, you have to let PHPStan know about it. Add something like this into your `phpstan.neon`: diff --git a/src/Type/Doctrine/DefaultDescriptorRegistry.php b/src/Type/Doctrine/DefaultDescriptorRegistry.php index 48886caa..2fc81131 100644 --- a/src/Type/Doctrine/DefaultDescriptorRegistry.php +++ b/src/Type/Doctrine/DefaultDescriptorRegistry.php @@ -36,4 +36,15 @@ public function get(string $type): DoctrineTypeDescriptor return $this->descriptors[$typeClass]; } + /** + * @throws DescriptorNotRegisteredException + */ + public function getByClassName(string $className): DoctrineTypeDescriptor + { + if (!isset($this->descriptors[$className])) { + throw new DescriptorNotRegisteredException(); + } + return $this->descriptors[$className]; + } + } diff --git a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php index 283d9506..8070a111 100644 --- a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php +++ b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php @@ -3,8 +3,12 @@ namespace PHPStan\Type\Doctrine\Descriptors; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Type as DbalType; +use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Type\Doctrine\DefaultDescriptorRegistry; +use PHPStan\Type\Doctrine\DescriptorNotRegisteredException; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -13,19 +17,27 @@ class ReflectionDescriptor implements DoctrineTypeDescriptor { - /** @var class-string<\Doctrine\DBAL\Types\Type> */ + /** @var class-string */ private $type; /** @var ReflectionProvider */ private $reflectionProvider; + /** @var Container */ + private $container; + /** - * @param class-string<\Doctrine\DBAL\Types\Type> $type + * @param class-string $type */ - public function __construct(string $type, ReflectionProvider $reflectionProvider) + public function __construct( + string $type, + ReflectionProvider $reflectionProvider, + Container $container + ) { $this->type = $type; $this->reflectionProvider = $reflectionProvider; + $this->container = $container; } public function getType(): string @@ -57,6 +69,20 @@ public function getWritableToDatabaseType(): Type public function getDatabaseInternalType(): Type { + $registry = $this->container->getByType(DefaultDescriptorRegistry::class); + $parents = $this->reflectionProvider->getClass($this->type)->getParentClassesNames(); + + foreach ($parents as $dbalTypeParentClass) { + try { + // this assumes that if somebody inherits from DecimalType, + // the real database type remains decimal and we can reuse its descriptor + return $registry->getByClassName($dbalTypeParentClass)->getDatabaseInternalType(); + + } catch (DescriptorNotRegisteredException $e) { + continue; + } + } + return new MixedType(); } diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 57561ef4..46d455a4 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -76,10 +76,10 @@ protected function getRule(): Rule new StringType(), new SimpleArrayType(), new UuidTypeDescriptor(FakeTestingUuidType::class), - new ReflectionDescriptor(CarbonImmutableType::class, $this->createBroker()), - new ReflectionDescriptor(CarbonType::class, $this->createBroker()), - new ReflectionDescriptor(CustomType::class, $this->createBroker()), - new ReflectionDescriptor(CustomNumericType::class, $this->createBroker()), + new ReflectionDescriptor(CarbonImmutableType::class, $this->createBroker(), self::getContainer()), + new ReflectionDescriptor(CarbonType::class, $this->createBroker(), self::getContainer()), + new ReflectionDescriptor(CustomType::class, $this->createBroker(), self::getContainer()), + new ReflectionDescriptor(CustomNumericType::class, $this->createBroker(), self::getContainer()), ]), $this->createReflectionProvider(), true, From ed7f604971e55972ff7c16be2b788cfca7eb20b9 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 25 Jun 2024 12:03:09 +0200 Subject: [PATCH 2/2] Guard by hasClass --- src/Type/Doctrine/Descriptors/ReflectionDescriptor.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php index 8070a111..82c44482 100644 --- a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php +++ b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php @@ -69,6 +69,10 @@ public function getWritableToDatabaseType(): Type public function getDatabaseInternalType(): Type { + if (!$this->reflectionProvider->hasClass($this->type)) { + return new MixedType(); + } + $registry = $this->container->getByType(DefaultDescriptorRegistry::class); $parents = $this->reflectionProvider->getClass($this->type)->getParentClassesNames();