From 2b3a74c33ab4cae7302a6269e8f5ed8f7b6cd3b6 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Wed, 18 Dec 2024 10:54:19 +0100 Subject: [PATCH 1/5] [PropertyInfo] Fix add missing composer conflict --- composer.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 0b880b7..495b51d 100644 --- a/composer.json +++ b/composer.json @@ -38,8 +38,9 @@ "doctrine/annotations": "<1.12", "phpdocumentor/reflection-docblock": "<5.2", "phpdocumentor/type-resolver": "<1.5.1", - "symfony/dependency-injection": "<5.4", - "symfony/dependency-injection": "<5.4|>=6.0,<6.4" + "symfony/dependency-injection": "<5.4|>=6.0,<6.4", + "symfony/cache": "<5.4", + "symfony/serializer": "<5.4" }, "autoload": { "psr-4": { "Symfony\\Component\\PropertyInfo\\": "" }, From ff6dda258c5cf3a9066de9c5ff534a83b72741a8 Mon Sep 17 00:00:00 2001 From: wuchen90 Date: Mon, 6 Jan 2025 12:19:17 +0100 Subject: [PATCH 2/5] fix(property-info): make sure that SerializerExtractor returns null for invalid class metadata --- Extractor/SerializerExtractor.php | 2 +- Tests/Extractor/SerializerExtractorTest.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Extractor/SerializerExtractor.php b/Extractor/SerializerExtractor.php index 7ef020c..0445b0b 100644 --- a/Extractor/SerializerExtractor.php +++ b/Extractor/SerializerExtractor.php @@ -34,7 +34,7 @@ public function getProperties(string $class, array $context = []): ?array return null; } - if (!$this->classMetadataFactory->getMetadataFor($class)) { + if (!$this->classMetadataFactory->hasMetadataFor($class)) { return null; } diff --git a/Tests/Extractor/SerializerExtractorTest.php b/Tests/Extractor/SerializerExtractorTest.php index 53d3396..433bdd6 100644 --- a/Tests/Extractor/SerializerExtractorTest.php +++ b/Tests/Extractor/SerializerExtractorTest.php @@ -55,4 +55,9 @@ public function testGetPropertiesWithAnyGroup() { $this->assertSame(['analyses', 'feet'], $this->extractor->getProperties(AdderRemoverDummy::class, ['serializer_groups' => null])); } + + public function testGetPropertiesWithNonExistentClassReturnsNull() + { + $this->assertSame(null, $this->extractor->getProperties('NonExistent')); + } } From 7a0518d18967c3bb73864c06af5647d6c13cb3d0 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Thu, 16 Jan 2025 19:46:02 +0100 Subject: [PATCH 3/5] [PropertyInfo] Add missing test --- Tests/Extractor/ReflectionExtractorTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/Extractor/ReflectionExtractorTest.php b/Tests/Extractor/ReflectionExtractorTest.php index 4556509..b2d9752 100644 --- a/Tests/Extractor/ReflectionExtractorTest.php +++ b/Tests/Extractor/ReflectionExtractorTest.php @@ -631,6 +631,17 @@ public static function writeMutatorProvider(): array ]; } + public function testDisabledAdderAndRemoverReturnsError() + { + $writeMutator = $this->extractor->getWriteInfo(Php71Dummy::class, 'baz', [ + 'enable_adder_remover_extraction' => false, + ]); + + self::assertNotNull($writeMutator); + self::assertSame(PropertyWriteInfo::TYPE_NONE, $writeMutator->getType()); + self::assertSame([\sprintf('The property "baz" in class "%s" can be defined with the methods "addBaz()", "removeBaz()" but the new value must be an array or an instance of \Traversable', Php71Dummy::class)], $writeMutator->getErrors()); + } + public function testGetWriteInfoReadonlyProperties() { $writeMutatorConstructor = $this->extractor->getWriteInfo(Php81Dummy::class, 'foo', ['enable_constructor_extraction' => true]); From fbefa5020e47f7b799aa7951cc4e884837688c25 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 19 Jun 2024 22:26:42 +0200 Subject: [PATCH 4/5] convert legacy types to TypeInfo types if getType() is not implemented --- PropertyInfoCacheExtractor.php | 36 ++++++++- PropertyInfoExtractor.php | 19 ++++- Tests/PropertyInfoCacheExtractorTest.php | 92 +++++++++++++++++++++++ Tests/PropertyInfoExtractorTest.php | 67 +++++++++++++++++ Util/LegacyTypeConverter.php | 94 ++++++++++++++++++++++++ 5 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 Util/LegacyTypeConverter.php diff --git a/PropertyInfoCacheExtractor.php b/PropertyInfoCacheExtractor.php index 38b9c68..caf9bd1 100644 --- a/PropertyInfoCacheExtractor.php +++ b/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/PropertyInfoExtractor.php b/PropertyInfoExtractor.php index 8e8952c..b2bb878 100644 --- a/PropertyInfoExtractor.php +++ b/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/Tests/PropertyInfoCacheExtractorTest.php b/Tests/PropertyInfoCacheExtractorTest.php index f9b1a8f..a594de5 100644 --- a/Tests/PropertyInfoCacheExtractorTest.php +++ b/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/Tests/PropertyInfoExtractorTest.php b/Tests/PropertyInfoExtractorTest.php index 53c1b1d..ed76df5 100644 --- a/Tests/PropertyInfoExtractorTest.php +++ b/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/Util/LegacyTypeConverter.php b/Util/LegacyTypeConverter.php new file mode 100644 index 0000000..24cae60 --- /dev/null +++ b/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); + } +} From 94d18e5cc11a37fd92856d38b61d9cdf72536a1e Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 21 Jan 2025 11:52:27 +0100 Subject: [PATCH 5/5] [PropertyInfo] Fix `TypeTest` duplicated assert --- Tests/TypeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/TypeTest.php b/Tests/TypeTest.php index e871ed4..87b4987 100644 --- a/Tests/TypeTest.php +++ b/Tests/TypeTest.php @@ -75,7 +75,7 @@ public function testArrayCollection() $this->assertTrue($firstValueType->isCollection()); $this->assertEquals(Type::BUILTIN_TYPE_ARRAY, $secondValueType->getBuiltinType()); $this->assertFalse($secondValueType->isNullable()); - $this->assertTrue($firstValueType->isCollection()); + $this->assertTrue($secondValueType->isCollection()); } public function testInvalidCollectionValueArgument()