diff --git a/UPGRADE-5.3.md b/UPGRADE-5.3.md index cd7dda750e..18e85ca363 100644 --- a/UPGRADE-5.3.md +++ b/UPGRADE-5.3.md @@ -33,6 +33,11 @@ PhpunitBridge * Deprecated the `SetUpTearDownTrait` trait, use original methods with "void" return typehint. +PropertyInfo +------------ + +* Deprecated the `Type::getCollectionKeyType()` and `Type::getCollectionValueType()` methods, use `Type::getCollectionKeyTypes()` and `Type::getCollectionValueTypes()` instead. + Security -------- diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index a766e6b302..01fa820c39 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +5.3.0 +----- + +* Added support for multiple types for collection keys & values +* Deprecated the `Type::getCollectionKeyType()` and `Type::getCollectionValueType()` methods, use `Type::getCollectionKeyTypes()` and `Type::getCollectionValueTypes()` instead. + 5.2.0 ----- diff --git a/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php b/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php index 30382bec8d..4d1c516b49 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\PropertyInfo\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\PropertyInfo\Type; /** @@ -19,8 +20,16 @@ use Symfony\Component\PropertyInfo\Type; */ class TypeTest extends TestCase { - public function testConstruct() + use ExpectDeprecationTrait; + + /** + * @group legacy + */ + public function testLegacyConstruct() { + $this->expectDeprecation('Since symfony/property-info 5.3: The "Symfony\Component\PropertyInfo\Type::getCollectionKeyType()" method is deprecated, use "getCollectionKeyTypes()" instead.'); + $this->expectDeprecation('Since symfony/property-info 5.3: The "Symfony\Component\PropertyInfo\Type::getCollectionValueType()" method is deprecated, use "getCollectionValueTypes()" instead.'); + $type = new Type('object', true, 'ArrayObject', true, new Type('int'), new Type('string')); $this->assertEquals(Type::BUILTIN_TYPE_OBJECT, $type->getBuiltinType()); @@ -37,6 +46,26 @@ class TypeTest extends TestCase $this->assertEquals(Type::BUILTIN_TYPE_STRING, $collectionValueType->getBuiltinType()); } + public function testConstruct() + { + $type = new Type('object', true, 'ArrayObject', true, new Type('int'), new Type('string')); + + $this->assertEquals(Type::BUILTIN_TYPE_OBJECT, $type->getBuiltinType()); + $this->assertTrue($type->isNullable()); + $this->assertEquals('ArrayObject', $type->getClassName()); + $this->assertTrue($type->isCollection()); + + $collectionKeyTypes = $type->getCollectionKeyTypes(); + $this->assertIsArray($collectionKeyTypes); + $this->assertContainsOnlyInstancesOf('Symfony\Component\PropertyInfo\Type', $collectionKeyTypes); + $this->assertEquals(Type::BUILTIN_TYPE_INT, $collectionKeyTypes[0]->getBuiltinType()); + + $collectionValueTypes = $type->getCollectionValueTypes(); + $this->assertIsArray($collectionValueTypes); + $this->assertContainsOnlyInstancesOf('Symfony\Component\PropertyInfo\Type', $collectionValueTypes); + $this->assertEquals(Type::BUILTIN_TYPE_STRING, $collectionValueTypes[0]->getBuiltinType()); + } + public function testIterable() { $type = new Type('iterable'); @@ -49,4 +78,46 @@ class TypeTest extends TestCase $this->expectExceptionMessage('"foo" is not a valid PHP type.'); new Type('foo'); } + + public function testArrayCollection() + { + $type = new Type('array', false, null, true, [new Type('int'), new Type('string')], [new Type('object', false, \ArrayObject::class, true), new Type('array', false, null, true)]); + + $this->assertEquals(Type::BUILTIN_TYPE_ARRAY, $type->getBuiltinType()); + $this->assertFalse($type->isNullable()); + $this->assertTrue($type->isCollection()); + + [$firstKeyType, $secondKeyType] = $type->getCollectionKeyTypes(); + $this->assertEquals(Type::BUILTIN_TYPE_INT, $firstKeyType->getBuiltinType()); + $this->assertFalse($firstKeyType->isNullable()); + $this->assertFalse($firstKeyType->isCollection()); + $this->assertEquals(Type::BUILTIN_TYPE_STRING, $secondKeyType->getBuiltinType()); + $this->assertFalse($secondKeyType->isNullable()); + $this->assertFalse($secondKeyType->isCollection()); + + [$firstValueType, $secondValueType] = $type->getCollectionValueTypes(); + $this->assertEquals(Type::BUILTIN_TYPE_OBJECT, $firstValueType->getBuiltinType()); + $this->assertEquals(\ArrayObject::class, $firstValueType->getClassName()); + $this->assertFalse($firstValueType->isNullable()); + $this->assertTrue($firstValueType->isCollection()); + $this->assertEquals(Type::BUILTIN_TYPE_ARRAY, $secondValueType->getBuiltinType()); + $this->assertFalse($secondValueType->isNullable()); + $this->assertTrue($firstValueType->isCollection()); + } + + public function testInvalidCollectionArgument() + { + $this->expectException('TypeError'); + $this->expectExceptionMessage('"Symfony\Component\PropertyInfo\Type::validateCollectionArgument()": Argument #5 ($collectionKeyType) must be of type "Symfony\Component\PropertyInfo\Type[]", "Symfony\Component\PropertyInfo\Type" or "null", "stdClass" given.'); + + new Type('array', false, null, true, new \stdClass(), [new Type('string')]); + } + + public function testInvalidCollectionValueArgument() + { + $this->expectException('TypeError'); + $this->expectExceptionMessage('"Symfony\Component\PropertyInfo\Type::validateCollectionArgument()": Argument #5 ($collectionKeyType) must be of type "Symfony\Component\PropertyInfo\Type[]", "Symfony\Component\PropertyInfo\Type" or "null", array value "array" given.'); + + new Type('array', false, null, true, [new \stdClass()], [new Type('string')]); + } } diff --git a/src/Symfony/Component/PropertyInfo/Type.php b/src/Symfony/Component/PropertyInfo/Type.php index 582b98d641..93255b2a56 100644 --- a/src/Symfony/Component/PropertyInfo/Type.php +++ b/src/Symfony/Component/PropertyInfo/Type.php @@ -57,9 +57,12 @@ class Type private $collectionValueType; /** + * @param Type[]|Type|null $collectionKeyType + * @param Type[]|Type|null $collectionValueType + * * @throws \InvalidArgumentException */ - public function __construct(string $builtinType, bool $nullable = false, string $class = null, bool $collection = false, self $collectionKeyType = null, self $collectionValueType = null) + public function __construct(string $builtinType, bool $nullable = false, string $class = null, bool $collection = false, $collectionKeyType = null, $collectionValueType = null) { if (!\in_array($builtinType, self::$builtinTypes)) { throw new \InvalidArgumentException(sprintf('"%s" is not a valid PHP type.', $builtinType)); @@ -69,8 +72,31 @@ class Type $this->nullable = $nullable; $this->class = $class; $this->collection = $collection; - $this->collectionKeyType = $collectionKeyType; - $this->collectionValueType = $collectionValueType; + $this->collectionKeyType = $this->validateCollectionArgument($collectionKeyType, 5, '$collectionKeyType') ?? []; + $this->collectionValueType = $this->validateCollectionArgument($collectionValueType, 6, '$collectionValueType') ?? []; + } + + private function validateCollectionArgument($collectionArgument, int $argumentIndex, string $argumentName): ?array + { + if (null === $collectionArgument) { + return null; + } + + if (!\is_array($collectionArgument) && !$collectionArgument instanceof self) { + throw new \TypeError(sprintf('"%s()": Argument #%d (%s) must be of type "%s[]", "%s" or "null", "%s" given.', __METHOD__, $argumentIndex, $argumentName, self::class, self::class, get_debug_type($collectionArgument))); + } + + if (\is_array($collectionArgument)) { + foreach ($collectionArgument as $type) { + if (!$type instanceof self) { + throw new \TypeError(sprintf('"%s()": Argument #%d (%s) must be of type "%s[]", "%s" or "null", array value "%s" given.', __METHOD__, $argumentIndex, $argumentName, self::class, self::class, get_debug_type($collectionArgument))); + } + } + + return $collectionArgument; + } + + return [$collectionArgument]; } /** @@ -107,8 +133,33 @@ class Type * Gets collection key type. * * Only applicable for a collection type. + * + * @deprecated since Symfony 5.3, use "getCollectionKeyTypes()" instead */ public function getCollectionKeyType(): ?self + { + trigger_deprecation('symfony/property-info', '5.3', 'The "%s()" method is deprecated, use "getCollectionKeyTypes()" instead.', __METHOD__); + + $type = $this->getCollectionKeyTypes(); + if (0 === \count($type)) { + return null; + } + + if (\is_array($type)) { + [$type] = $type; + } + + return $type; + } + + /** + * Gets collection key types. + * + * Only applicable for a collection type. + * + * @return Type[] + */ + public function getCollectionKeyTypes(): array { return $this->collectionKeyType; } @@ -117,8 +168,33 @@ class Type * Gets collection value type. * * Only applicable for a collection type. + * + * @deprecated since Symfony 5.3, use "getCollectionValueTypes()" instead */ public function getCollectionValueType(): ?self + { + trigger_deprecation('symfony/property-info', '5.3', 'The "%s()" method is deprecated, use "getCollectionValueTypes()" instead.', __METHOD__); + + $type = $this->getCollectionValueTypes(); + if (0 === \count($type)) { + return null; + } + + if (\is_array($type)) { + [$type] = $type; + } + + return $type; + } + + /** + * Gets collection value types. + * + * Only applicable for a collection type. + * + * @return Type[] + */ + public function getCollectionValueTypes(): array { return $this->collectionValueType; } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index aa1be48cfb..36099aa385 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -373,7 +373,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer return null; } - $collectionValueType = $type->isCollection() ? $type->getCollectionValueType() : null; + $collectionValueType = $type->isCollection() ? $type->getCollectionValueTypes()[0] ?? null : null; // Fix a collection that contains the only one element // This is special to xml format only @@ -431,18 +431,18 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer $builtinType = Type::BUILTIN_TYPE_OBJECT; $class = $collectionValueType->getClassName().'[]'; - if (null !== $collectionKeyType = $type->getCollectionKeyType()) { - $context['key_type'] = $collectionKeyType; + if (null !== $collectionKeyType = $type->getCollectionKeyTypes()) { + [$context['key_type']] = $collectionKeyType; } - } elseif ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_ARRAY === $collectionValueType->getBuiltinType()) { + } elseif ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueTypes()) && \count($collectionValueType) > 0 && Type::BUILTIN_TYPE_ARRAY === $collectionValueType[0]->getBuiltinType()) { // get inner type for any nested array - $innerType = $collectionValueType; + [$innerType] = $collectionValueType; // note that it will break for any other builtinType $dimensions = '[]'; - while (null !== $innerType->getCollectionValueType() && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) { + while (null !== $innerType->getCollectionValueTypes() && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) { $dimensions .= '[]'; - $innerType = $innerType->getCollectionValueType(); + [$innerType] = $innerType->getCollectionValueTypes(); } if (null !== $innerType->getClassName()) { diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 60b8b5c054..b5221f69b9 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -34,7 +34,7 @@ "symfony/http-kernel": "^4.4|^5.0", "symfony/mime": "^4.4|^5.0", "symfony/property-access": "^4.4|^5.0", - "symfony/property-info": "^4.4|^5.0", + "symfony/property-info": "^5.3", "symfony/uid": "^5.1", "symfony/validator": "^4.4|^5.0", "symfony/var-exporter": "^4.4|^5.0", diff --git a/src/Symfony/Component/Validator/Mapping/Loader/PropertyInfoLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/PropertyInfoLoader.php index 530348c638..3f83da8d05 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/PropertyInfoLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/PropertyInfoLoader.php @@ -119,7 +119,8 @@ final class PropertyInfoLoader implements LoaderInterface } if (!$hasTypeConstraint) { if (1 === \count($builtinTypes)) { - if ($types[0]->isCollection() && (null !== $collectionValueType = $types[0]->getCollectionValueType())) { + if ($types[0]->isCollection() && (null !== $collectionValueType = $types[0]->getCollectionValueTypes())) { + [$collectionValueType] = $collectionValueType; $this->handleAllConstraint($property, $allConstraint, $collectionValueType, $metadata); } diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 3c4cc5090a..949b36d360 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -38,7 +38,7 @@ "symfony/cache": "^4.4|^5.0", "symfony/mime": "^4.4|^5.0", "symfony/property-access": "^4.4|^5.0", - "symfony/property-info": "^4.4|^5.0", + "symfony/property-info": "^5.3", "symfony/translation": "^4.4|^5.0", "doctrine/annotations": "~1.7", "doctrine/cache": "~1.0",