diff --git a/composer.json b/composer.json index 24ea4782..9185e8b9 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", - "symfony/cache": "^5.4" + "symfony/cache": "^5.4", + "symfony/uid": "^5.4 || ^6.4 || ^7.3" }, "config": { "sort-packages": true, diff --git a/extension.neon b/extension.neon index 803ffaed..046d05d6 100644 --- a/extension.neon +++ b/extension.neon @@ -418,6 +418,16 @@ services: tags: [phpstan.doctrine.typeDescriptor] arguments: uuidTypeName: Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType + - + class: PHPStan\Type\Doctrine\Descriptors\Symfony\UuidTypeDescriptor + tags: [phpstan.doctrine.typeDescriptor] + arguments: + uuidTypeName: Symfony\Bridge\Doctrine\Types\UuidType + - + class: PHPStan\Type\Doctrine\Descriptors\Symfony\UlidTypeDescriptor + tags: [phpstan.doctrine.typeDescriptor] + arguments: + uuidTypeName: Symfony\Bridge\Doctrine\Types\UlidType # Doctrine Collection - diff --git a/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php b/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php index 549b14c4..7b0d8bf5 100644 --- a/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php +++ b/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php @@ -2,46 +2,29 @@ namespace PHPStan\Type\Doctrine\Descriptors\Ramsey; -use PHPStan\Rules\Doctrine\ORM\FakeTestingUuidType; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use Ramsey\Uuid\UuidInterface; -use function in_array; -use function sprintf; class UuidTypeDescriptor implements DoctrineTypeDescriptor { - private const SUPPORTED_UUID_TYPES = [ - 'Ramsey\Uuid\Doctrine\UuidType', - 'Ramsey\Uuid\Doctrine\UuidBinaryType', - 'Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType', - FakeTestingUuidType::class, - ]; - + /** @var class-string<\Doctrine\DBAL\Types\Type> */ private string $uuidTypeName; - public function __construct( - string $uuidTypeName - ) + /** + * @param class-string<\Doctrine\DBAL\Types\Type> $uuidTypeName + */ + public function __construct(string $uuidTypeName) { - if (!in_array($uuidTypeName, self::SUPPORTED_UUID_TYPES, true)) { - throw new ShouldNotHappenException(sprintf( - 'Unexpected UUID column type "%s" provided', - $uuidTypeName, - )); - } - $this->uuidTypeName = $uuidTypeName; } public function getType(): string { - /** @var class-string<\Doctrine\DBAL\Types\Type> */ return $this->uuidTypeName; } diff --git a/src/Type/Doctrine/Descriptors/Symfony/UlidTypeDescriptor.php b/src/Type/Doctrine/Descriptors/Symfony/UlidTypeDescriptor.php new file mode 100644 index 00000000..58fb1083 --- /dev/null +++ b/src/Type/Doctrine/Descriptors/Symfony/UlidTypeDescriptor.php @@ -0,0 +1,49 @@ + */ + private string $uuidTypeName; + + /** + * @param class-string<\Doctrine\DBAL\Types\Type> $uuidTypeName + */ + public function __construct(string $uuidTypeName) + { + $this->uuidTypeName = $uuidTypeName; + } + + public function getType(): string + { + return $this->uuidTypeName; + } + + public function getWritableToPropertyType(): Type + { + return new ObjectType(Ulid::class); + } + + public function getWritableToDatabaseType(): Type + { + return TypeCombinator::union( + new StringType(), + new ObjectType(Ulid::class), + ); + } + + public function getDatabaseInternalType(): Type + { + return new StringType(); + } + +} diff --git a/src/Type/Doctrine/Descriptors/Symfony/UuidTypeDescriptor.php b/src/Type/Doctrine/Descriptors/Symfony/UuidTypeDescriptor.php new file mode 100644 index 00000000..03151cec --- /dev/null +++ b/src/Type/Doctrine/Descriptors/Symfony/UuidTypeDescriptor.php @@ -0,0 +1,49 @@ + */ + private string $uuidTypeName; + + /** + * @param class-string<\Doctrine\DBAL\Types\Type> $uuidTypeName + */ + public function __construct(string $uuidTypeName) + { + $this->uuidTypeName = $uuidTypeName; + } + + public function getType(): string + { + return $this->uuidTypeName; + } + + public function getWritableToPropertyType(): Type + { + return new ObjectType(Uuid::class); + } + + public function getWritableToDatabaseType(): Type + { + return TypeCombinator::union( + new StringType(), + new ObjectType(Uuid::class), + ); + } + + public function getDatabaseInternalType(): Type + { + return new StringType(); + } + +} diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index c55b5cd5..fbf03af8 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -21,10 +21,12 @@ use PHPStan\Type\Doctrine\Descriptors\EnumType; use PHPStan\Type\Doctrine\Descriptors\IntegerType; use PHPStan\Type\Doctrine\Descriptors\JsonType; -use PHPStan\Type\Doctrine\Descriptors\Ramsey\UuidTypeDescriptor; +use PHPStan\Type\Doctrine\Descriptors\Ramsey\UuidTypeDescriptor as RamseyUuidTypeDescriptor; use PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor; use PHPStan\Type\Doctrine\Descriptors\SimpleArrayType; use PHPStan\Type\Doctrine\Descriptors\StringType; +use PHPStan\Type\Doctrine\Descriptors\Symfony\UlidTypeDescriptor as SymfonyUlidTypeDescriptor; +use PHPStan\Type\Doctrine\Descriptors\Symfony\UuidTypeDescriptor as SymfonyUuidTypeDescriptor; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use function array_unshift; use function class_exists; @@ -41,6 +43,8 @@ class EntityColumnRuleTest extends RuleTestCase private ?string $objectManagerLoader = null; + private bool $useSymfonyUuid = false; + protected function getRule(): Rule { if (!Type::hasType(CustomType::NAME)) { @@ -49,8 +53,23 @@ protected function getRule(): Rule if (!Type::hasType(CustomNumericType::NAME)) { Type::addType(CustomNumericType::NAME, CustomNumericType::class); } - if (!Type::hasType(FakeTestingUuidType::NAME)) { - Type::addType(FakeTestingUuidType::NAME, FakeTestingUuidType::class); + if ($this->useSymfonyUuid) { + if (!Type::hasType(FakeTestingSymfonyUuidType::NAME)) { + Type::addType(FakeTestingSymfonyUuidType::NAME, FakeTestingSymfonyUuidType::class); + } else { + // Override Ramsay definition + Type::overrideType(FakeTestingSymfonyUuidType::NAME, FakeTestingSymfonyUuidType::class); + } + if (!Type::hasType(FakeTestingSymfonyUlidType::NAME)) { + Type::addType(FakeTestingSymfonyUlidType::NAME, FakeTestingSymfonyUlidType::class); + } + } else { + if (!Type::hasType(FakeTestingRamseyUuidType::NAME)) { + Type::addType(FakeTestingRamseyUuidType::NAME, FakeTestingRamseyUuidType::class); + } else { + // Override Symfony definition + Type::overrideType(FakeTestingRamseyUuidType::NAME, FakeTestingRamseyUuidType::class); + } } if (!Type::hasType('carbon')) { Type::addType('carbon', CarbonType::class); @@ -76,8 +95,10 @@ protected function getRule(): Rule new IntegerType(), new StringType(), new SimpleArrayType(), - new UuidTypeDescriptor(FakeTestingUuidType::class), new EnumType(), + new RamseyUuidTypeDescriptor(FakeTestingRamseyUuidType::class), + new SymfonyUuidTypeDescriptor(FakeTestingSymfonyUuidType::class), + new SymfonyUlidTypeDescriptor(FakeTestingSymfonyUlidType::class), new ReflectionDescriptor(CarbonImmutableType::class, $this->createReflectionProvider(), self::getContainer()), new ReflectionDescriptor(CarbonType::class, $this->createReflectionProvider(), self::getContainer()), new ReflectionDescriptor(CustomType::class, $this->createReflectionProvider(), self::getContainer()), @@ -461,6 +482,27 @@ public function testBug677(?string $objectManagerLoader): void $this->analyse([__DIR__ . '/data/bug-677.php'], []); } + /** + * @dataProvider dataObjectManagerLoader + */ + public function testSymfonyUuid(?string $objectManagerLoader): void + { + $this->allowNullablePropertyForRequiredField = true; + $this->objectManagerLoader = $objectManagerLoader; + $this->useSymfonyUuid = true; + + $this->analyse([__DIR__ . '/data/EntityWithSymfonyUid.php'], [ + [ + 'Property PHPStan\Rules\Doctrine\ORM\EntityWithSymfonyUid::$uuidInvalidType type mapping mismatch: database can contain Symfony\Component\Uid\Uuid but property expects string.', + 32, + ], + [ + 'Property PHPStan\Rules\Doctrine\ORM\EntityWithSymfonyUid::$ulidInvalidType type mapping mismatch: database can contain Symfony\Component\Uid\Ulid but property expects string.', + 44, + ], + ]); + } + /** * @dataProvider dataObjectManagerLoader */ diff --git a/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php b/tests/Rules/Doctrine/ORM/FakeTestingRamseyUuidType.php similarity index 96% rename from tests/Rules/Doctrine/ORM/FakeTestingUuidType.php rename to tests/Rules/Doctrine/ORM/FakeTestingRamseyUuidType.php index f6255563..a81fdc64 100644 --- a/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php +++ b/tests/Rules/Doctrine/ORM/FakeTestingRamseyUuidType.php @@ -13,7 +13,7 @@ * From https://github.com/ramsey/uuid-doctrine/blob/fafebbe972cdaba9274c286ea8923e2de2579027/src/UuidType.php * Copyright (c) 2012-2022 Ben Ramsey */ -final class FakeTestingUuidType extends GuidType +final class FakeTestingRamseyUuidType extends GuidType { public const NAME = 'uuid'; diff --git a/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php new file mode 100644 index 00000000..389303bf --- /dev/null +++ b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php @@ -0,0 +1,113 @@ + + */ +final class FakeTestingSymfonyUlidType extends Type +{ + + public const NAME = 'ulid'; + + /** + * @not-deprecated + */ + public function getName(): string + { + return self::NAME; + } + + protected function getUidClass(): string + { + return Ulid::class; + } + + /** + * {@inheritdoc} + */ + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string + { + if ($this->hasNativeGuidType($platform)) { + return $platform->getGuidTypeDeclarationSQL($column); + } + + return $platform->getBinaryTypeDeclarationSQL([ + 'length' => '16', + 'fixed' => true, + ]); + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?AbstractUid + { + if ($value instanceof AbstractUid || $value === null) { + return $value; + } + + if (!is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]); + } + + try { + /** @phpstan-ignore-next-line method.dynamicName */ + return $this->getUidClass()::fromString($value); + } catch (InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName(), $e); + } + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + $toString = $this->hasNativeGuidType($platform) ? 'toRfc4122' : 'toBinary'; + + if ($value instanceof AbstractUid) { + /** @phpstan-ignore-next-line method.dynamicName */ + return $value->$toString(); + } + + if ($value === null || $value === '') { + return null; + } + + if (!is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]); + } + + try { + /** @phpstan-ignore-next-line method.dynamicName */ + return $this->getUidClass()::fromString($value)->$toString(); + } catch (InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + } + + public function requiresSQLCommentHint(AbstractPlatform $platform): bool + { + return true; + } + + private function hasNativeGuidType(AbstractPlatform $platform): bool + { + return $platform->getGuidTypeDeclarationSQL([]) !== $platform->getStringTypeDeclarationSQL(['fixed' => true, 'length' => 36]); + } + +} diff --git a/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php new file mode 100644 index 00000000..89d853ef --- /dev/null +++ b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php @@ -0,0 +1,113 @@ + + */ +final class FakeTestingSymfonyUuidType extends Type +{ + + public const NAME = 'uuid'; + + /** + * @not-deprecated + */ + public function getName(): string + { + return self::NAME; + } + + protected function getUidClass(): string + { + return Uuid::class; + } + + /** + * {@inheritdoc} + */ + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string + { + if ($this->hasNativeGuidType($platform)) { + return $platform->getGuidTypeDeclarationSQL($column); + } + + return $platform->getBinaryTypeDeclarationSQL([ + 'length' => '16', + 'fixed' => true, + ]); + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?AbstractUid + { + if ($value instanceof AbstractUid || $value === null) { + return $value; + } + + if (!is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]); + } + + try { + /** @phpstan-ignore-next-line method.dynamicName */ + return $this->getUidClass()::fromString($value); + } catch (InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName(), $e); + } + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + $toString = $this->hasNativeGuidType($platform); + + if ($value instanceof AbstractUid) { + /** @phpstan-ignore-next-line method.dynamicName */ + return $value->$toString(); + } + + if ($value === null || $value === '') { + return null; + } + + if (!is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]); + } + + try { + /** @phpstan-ignore-next-line method.dynamicName */ + return $this->getUidClass()::fromString($value)->$toString(); + } catch (InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + } + + public function requiresSQLCommentHint(AbstractPlatform $platform): bool + { + return true; + } + + private function hasNativeGuidType(AbstractPlatform $platform): bool + { + return $platform->getGuidTypeDeclarationSQL([]) !== $platform->getStringTypeDeclarationSQL(['fixed' => true, 'length' => 36]); + } + +} diff --git a/tests/Rules/Doctrine/ORM/data/EntityWithSymfonyUid.php b/tests/Rules/Doctrine/ORM/data/EntityWithSymfonyUid.php new file mode 100644 index 00000000..a1c742e2 --- /dev/null +++ b/tests/Rules/Doctrine/ORM/data/EntityWithSymfonyUid.php @@ -0,0 +1,45 @@ +