Skip to content

Commit

Permalink
convert legacy types to TypeInfo types if getType() is not implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
xabbuh committed Jun 20, 2024
1 parent 7abc106 commit c8c4b55
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\PropertyInfo;

use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\PropertyInfo\Util\LegacyTypeConverter;
use Symfony\Component\TypeInfo\Type;

/**
Expand Down Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\PropertyInfo;

use Symfony\Component\PropertyInfo\Util\LegacyTypeConverter;
use Symfony\Component\TypeInfo\Type;

/**
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dunglas@gmail.com>
Expand Down Expand Up @@ -76,4 +82,88 @@ public function testIsInitializable()
parent::testIsInitializable();
parent::testIsInitializable();
}

/**
* @dataProvider provideNestedExtractourWithoutGetTypeImplementationData
*/
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 provideNestedExtractourWithoutGetTypeImplementationData()
{
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)];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,74 @@

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 <dunglas@gmail.com>
*/
class PropertyInfoExtractorTest extends AbstractPropertyInfoExtractorTest
{
/**
* @dataProvider provideNestedExtractourWithoutGetTypeImplementationData
*/
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 provideNestedExtractourWithoutGetTypeImplementationData()
{
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)];
}
}
69 changes: 69 additions & 0 deletions src/Symfony/Component/PropertyInfo/Util/LegacyTypeConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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) {
if ($legacyType->isCollection() && !\in_array($legacyType->getBuiltinType(), [LegacyType::BUILTIN_TYPE_ARRAY, LegacyType::BUILTIN_TYPE_ITERABLE], true)) {
$typeInfoType = Type::collection(Type::object(\Iterator::class), self::toTypeInfoType($legacyType->getCollectionValueTypes()), self::toTypeInfoType($legacyType->getCollectionKeyTypes()));
} else {
$typeInfoType = match ($legacyType->getBuiltinType()) {
LegacyType::BUILTIN_TYPE_ARRAY => Type::array(self::toTypeInfoType($legacyType->getCollectionValueTypes()), self::toTypeInfoType($legacyType->getCollectionKeyTypes())),
LegacyType::BUILTIN_TYPE_BOOL => Type::bool(),
LegacyType::BUILTIN_TYPE_CALLABLE => Type::callable(),
LegacyType::BUILTIN_TYPE_FALSE => Type::false(),
LegacyType::BUILTIN_TYPE_FLOAT => Type::float(),
LegacyType::BUILTIN_TYPE_INT => Type::int(),
LegacyType::BUILTIN_TYPE_ITERABLE => Type::iterable(self::toTypeInfoType($legacyType->getCollectionValueTypes()), self::toTypeInfoType($legacyType->getCollectionKeyTypes())),
LegacyType::BUILTIN_TYPE_OBJECT => Type::object($legacyType->getClassName()),
LegacyType::BUILTIN_TYPE_RESOURCE => Type::resource(),
LegacyType::BUILTIN_TYPE_STRING => Type::string(),
LegacyType::BUILTIN_TYPE_TRUE => Type::true(),
default => null,
};
}

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);
}
}

0 comments on commit c8c4b55

Please sign in to comment.