From 98550a98f74be945b06227abad2bf0e1feb868e6 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 19 Jun 2024 22:26:42 +0200 Subject: [PATCH] convert legacy types to TypeInfo types if getType() is not implemented --- .../PropertyInfoCacheExtractor.php | 36 ++++++- .../PropertyInfo/PropertyInfoExtractor.php | 19 +++- .../Tests/PropertyInfoCacheExtractorTest.php | 92 ++++++++++++++++++ .../Tests/PropertyInfoExtractorTest.php | 67 +++++++++++++ .../PropertyInfo/Util/LegacyTypeConverter.php | 94 +++++++++++++++++++ 5 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Component/PropertyInfo/Util/LegacyTypeConverter.php diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php index 38b9c68a2e29e..caf9bd182233f 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php @@ -12,6 +12,7 @@ namespace Symfony\Component\PropertyInfo; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\PropertyInfo\Util\LegacyTypeConverter; use Symfony\Component\TypeInfo\Type; /** @@ -61,7 +62,40 @@ public function getProperties(string $class, array $context = []): ?array */ public function getType(string $class, string $property, array $context = []): ?Type { - return $this->extract('getType', [$class, $property, $context]); + try { + $serializedArguments = serialize([$class, $property, $context]); + } catch (\Exception) { + // If arguments are not serializable, skip the cache + if (method_exists($this->propertyInfoExtractor, 'getType')) { + return $this->propertyInfoExtractor->getType($class, $property, $context); + } + + return LegacyTypeConverter::toTypeInfoType($this->propertyInfoExtractor->getTypes($class, $property, $context)); + } + + // Calling rawurlencode escapes special characters not allowed in PSR-6's keys + $key = rawurlencode('getType.'.$serializedArguments); + + if (\array_key_exists($key, $this->arrayCache)) { + return $this->arrayCache[$key]; + } + + $item = $this->cacheItemPool->getItem($key); + + if ($item->isHit()) { + return $this->arrayCache[$key] = $item->get(); + } + + if (method_exists($this->propertyInfoExtractor, 'getType')) { + $value = $this->propertyInfoExtractor->getType($class, $property, $context); + } else { + $value = LegacyTypeConverter::toTypeInfoType($this->propertyInfoExtractor->getTypes($class, $property, $context)); + } + + $item->set($value); + $this->cacheItemPool->save($item); + + return $this->arrayCache[$key] = $value; } public function getTypes(string $class, string $property, array $context = []): ?array diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php index 8e8952c7f4e23..b2bb878496221 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php @@ -11,6 +11,7 @@ namespace Symfony\Component\PropertyInfo; +use Symfony\Component\PropertyInfo\Util\LegacyTypeConverter; use Symfony\Component\TypeInfo\Type; /** @@ -58,7 +59,23 @@ public function getLongDescription(string $class, string $property, array $conte */ public function getType(string $class, string $property, array $context = []): ?Type { - return $this->extract($this->typeExtractors, 'getType', [$class, $property, $context]); + foreach ($this->typeExtractors as $extractor) { + if (!method_exists($extractor, 'getType')) { + $legacyTypes = $extractor->getTypes($class, $property, $context); + + if (null !== $legacyTypes) { + return LegacyTypeConverter::toTypeInfoType($legacyTypes); + } + + continue; + } + + if (null !== $value = $extractor->getType($class, $property, $context)) { + return $value; + } + } + + return null; } public function getTypes(string $class, string $property, array $context = []): ?array diff --git a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php index f9b1a8fc3358e..a594de56a5e42 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php @@ -12,7 +12,13 @@ namespace Symfony\Component\PropertyInfo\Tests; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; +use Symfony\Component\TypeInfo\Type; /** * @author Kévin Dunglas @@ -76,4 +82,90 @@ public function testIsInitializable() parent::testIsInitializable(); parent::testIsInitializable(); } + + /** + * @group legacy + * + * @dataProvider provideNestedExtractorWithoutGetTypeImplementationData + */ + public function testNestedExtractorWithoutGetTypeImplementation(string $property, ?Type $expectedType) + { + $propertyInfoCacheExtractor = new PropertyInfoCacheExtractor(new class() implements PropertyInfoExtractorInterface { + private PropertyTypeExtractorInterface $propertyTypeExtractor; + + public function __construct() + { + $this->propertyTypeExtractor = new PhpDocExtractor(); + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + return $this->propertyTypeExtractor->getTypes($class, $property, $context); + } + + public function isReadable(string $class, string $property, array $context = []): ?bool + { + return null; + } + + public function isWritable(string $class, string $property, array $context = []): ?bool + { + return null; + } + + public function getShortDescription(string $class, string $property, array $context = []): ?string + { + return null; + } + + public function getLongDescription(string $class, string $property, array $context = []): ?string + { + return null; + } + + public function getProperties(string $class, array $context = []): ?array + { + return null; + } + }, new ArrayAdapter()); + + if (null === $expectedType) { + $this->assertNull($propertyInfoCacheExtractor->getType(Dummy::class, $property)); + } else { + $this->assertEquals($expectedType, $propertyInfoCacheExtractor->getType(Dummy::class, $property)); + } + } + + public function provideNestedExtractorWithoutGetTypeImplementationData() + { + yield ['bar', Type::string()]; + yield ['baz', Type::int()]; + yield ['bal', Type::object(\DateTimeImmutable::class)]; + yield ['parent', Type::object(ParentDummy::class)]; + yield ['collection', Type::array(Type::object(\DateTimeImmutable::class), Type::int())]; + yield ['nestedCollection', Type::array(Type::array(Type::string(), Type::int()), Type::int())]; + yield ['mixedCollection', Type::array()]; + yield ['B', Type::object(ParentDummy::class)]; + yield ['Id', Type::int()]; + yield ['Guid', Type::string()]; + yield ['g', Type::nullable(Type::array())]; + yield ['h', Type::nullable(Type::string())]; + yield ['i', Type::nullable(Type::union(Type::string(), Type::int()))]; + yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class))]; + yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::array(Type::int(), Type::int()))]; + yield ['nonNullableCollectionOfNullableElements', Type::array(Type::nullable(Type::int()), Type::int())]; + yield ['nullableCollectionOfMultipleNonNullableElementTypes', Type::nullable(Type::array(Type::union(Type::int(), Type::string()), Type::int()))]; + yield ['xTotals', Type::array()]; + yield ['YT', Type::string()]; + yield ['emptyVar', null]; + yield ['iteratorCollection', Type::collection(Type::object(\Iterator::class), Type::string(), Type::union(Type::string(), Type::int()))]; + yield ['iteratorCollectionWithKey', Type::collection(Type::object(\Iterator::class), Type::string(), Type::int())]; + yield ['nestedIterators', Type::collection(Type::object(\Iterator::class), Type::collection(Type::object(\Iterator::class), Type::string(), Type::int()), Type::int())]; + yield ['arrayWithKeys', Type::array(Type::string(), Type::string())]; + yield ['arrayWithKeysAndComplexValue', Type::array(Type::nullable(Type::array(Type::nullable(Type::string()), Type::int())), Type::string())]; + yield ['arrayOfMixed', Type::array(Type::mixed(), Type::string())]; + yield ['noDocBlock', null]; + yield ['listOfStrings', Type::array(Type::string(), Type::int())]; + yield ['parentAnnotation', Type::object(ParentDummy::class)]; + } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoExtractorTest.php index 53c1b1d8a5c22..ed76df5695b08 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoExtractorTest.php @@ -11,9 +11,76 @@ namespace Symfony\Component\PropertyInfo\Tests; +use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; +use Symfony\Component\TypeInfo\Type; + /** * @author Kévin Dunglas */ class PropertyInfoExtractorTest extends AbstractPropertyInfoExtractorTest { + /** + * @group legacy + * + * @dataProvider provideNestedExtractorWithoutGetTypeImplementationData + */ + public function testNestedExtractorWithoutGetTypeImplementation(string $property, ?Type $expectedType) + { + $propertyInfoExtractor = new PropertyInfoExtractor([], [new class() implements PropertyTypeExtractorInterface { + private PropertyTypeExtractorInterface $propertyTypeExtractor; + + public function __construct() + { + $this->propertyTypeExtractor = new PhpDocExtractor(); + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + return $this->propertyTypeExtractor->getTypes($class, $property, $context); + } + }]); + + if (null === $expectedType) { + $this->assertNull($propertyInfoExtractor->getType(Dummy::class, $property)); + } else { + $this->assertEquals($expectedType, $propertyInfoExtractor->getType(Dummy::class, $property)); + } + } + + public function provideNestedExtractorWithoutGetTypeImplementationData() + { + yield ['bar', Type::string()]; + yield ['baz', Type::int()]; + yield ['bal', Type::object(\DateTimeImmutable::class)]; + yield ['parent', Type::object(ParentDummy::class)]; + yield ['collection', Type::array(Type::object(\DateTimeImmutable::class), Type::int())]; + yield ['nestedCollection', Type::array(Type::array(Type::string(), Type::int()), Type::int())]; + yield ['mixedCollection', Type::array()]; + yield ['B', Type::object(ParentDummy::class)]; + yield ['Id', Type::int()]; + yield ['Guid', Type::string()]; + yield ['g', Type::nullable(Type::array())]; + yield ['h', Type::nullable(Type::string())]; + yield ['i', Type::nullable(Type::union(Type::string(), Type::int()))]; + yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class))]; + yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::array(Type::int(), Type::int()))]; + yield ['nonNullableCollectionOfNullableElements', Type::array(Type::nullable(Type::int()), Type::int())]; + yield ['nullableCollectionOfMultipleNonNullableElementTypes', Type::nullable(Type::array(Type::union(Type::int(), Type::string()), Type::int()))]; + yield ['xTotals', Type::array()]; + yield ['YT', Type::string()]; + yield ['emptyVar', null]; + yield ['iteratorCollection', Type::collection(Type::object(\Iterator::class), Type::string(), Type::union(Type::string(), Type::int()))]; + yield ['iteratorCollectionWithKey', Type::collection(Type::object(\Iterator::class), Type::string(), Type::int())]; + yield ['nestedIterators', Type::collection(Type::object(\Iterator::class), Type::collection(Type::object(\Iterator::class), Type::string(), Type::int()), Type::int())]; + yield ['arrayWithKeys', Type::array(Type::string(), Type::string())]; + yield ['arrayWithKeysAndComplexValue', Type::array(Type::nullable(Type::array(Type::nullable(Type::string()), Type::int())), Type::string())]; + yield ['arrayOfMixed', Type::array(Type::mixed(), Type::string())]; + yield ['noDocBlock', null]; + yield ['listOfStrings', Type::array(Type::string(), Type::int())]; + yield ['parentAnnotation', Type::object(ParentDummy::class)]; + } } diff --git a/src/Symfony/Component/PropertyInfo/Util/LegacyTypeConverter.php b/src/Symfony/Component/PropertyInfo/Util/LegacyTypeConverter.php new file mode 100644 index 0000000000000..24cae602aac5b --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Util/LegacyTypeConverter.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Util; + +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; + +/** + * @internal + */ +class LegacyTypeConverter +{ + /** + * @param LegacyType[]|null $legacyTypes + */ + public static function toTypeInfoType(?array $legacyTypes): ?Type + { + if (null === $legacyTypes || [] === $legacyTypes) { + return null; + } + + $nullable = false; + $types = []; + + foreach ($legacyTypes as $legacyType) { + switch ($legacyType->getBuiltinType()) { + case LegacyType::BUILTIN_TYPE_ARRAY: + $typeInfoType = Type::array(self::toTypeInfoType($legacyType->getCollectionValueTypes()), self::toTypeInfoType($legacyType->getCollectionKeyTypes())); + break; + case LegacyType::BUILTIN_TYPE_BOOL: + $typeInfoType = Type::bool(); + break; + case LegacyType::BUILTIN_TYPE_CALLABLE: + $typeInfoType = Type::callable(); + break; + case LegacyType::BUILTIN_TYPE_FALSE: + $typeInfoType = Type::false(); + break; + case LegacyType::BUILTIN_TYPE_FLOAT: + $typeInfoType = Type::float(); + break; + case LegacyType::BUILTIN_TYPE_INT: + $typeInfoType = Type::int(); + break; + case LegacyType::BUILTIN_TYPE_ITERABLE: + $typeInfoType = Type::iterable(self::toTypeInfoType($legacyType->getCollectionValueTypes()), self::toTypeInfoType($legacyType->getCollectionKeyTypes())); + break; + case LegacyType::BUILTIN_TYPE_OBJECT: + if ($legacyType->isCollection()) { + $typeInfoType = Type::collection(Type::object($legacyType->getClassName()), self::toTypeInfoType($legacyType->getCollectionValueTypes()), self::toTypeInfoType($legacyType->getCollectionKeyTypes())); + } else { + $typeInfoType = Type::object($legacyType->getClassName()); + } + + break; + case LegacyType::BUILTIN_TYPE_RESOURCE: + $typeInfoType = Type::resource(); + break; + case LegacyType::BUILTIN_TYPE_STRING: + $typeInfoType = Type::string(); + break; + case LegacyType::BUILTIN_TYPE_TRUE: + $typeInfoType = Type::true(); + break; + default: + $typeInfoType = null; + break; + } + + if (LegacyType::BUILTIN_TYPE_NULL === $legacyType->getBuiltinType() || $legacyType->isNullable()) { + $nullable = true; + } + + if (null !== $typeInfoType) { + $types[] = $typeInfoType; + } + } + + if (1 === \count($types)) { + return $nullable ? Type::nullable($types[0]) : $types[0]; + } + + return $nullable ? Type::nullable(Type::union(...$types)) : Type::union(...$types); + } +}