Support multiple types for collection keys & values

This commit is contained in:
Baptiste Leduc 2020-11-06 20:34:22 +01:00
parent e1e1defffa
commit 84dd1784cb
No known key found for this signature in database
GPG Key ID: E35B5D50B5BE1A8A
8 changed files with 173 additions and 14 deletions

View File

@ -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
--------

View File

@ -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
-----

View File

@ -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')]);
}
}

View File

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

View File

@ -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()) {

View File

@ -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",

View File

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

View File

@ -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",