8000 Support multiple types for collection keys & values · symfony/symfony@28248c6 · GitHub
[go: up one dir, main page]

Skip to content

Commit 28248c6

Browse files
committed
Support multiple types for collection keys & values
1 parent dc0d45d commit 28248c6

File tree

8 files changed

+175
-14
lines changed

8 files changed

+175
-14
lines changed

UPGRADE-5.3.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
UPGRADE FROM 5.2 to 5.3
2+
=======================
3+
4+
PropertyInfo
5+
------------
6+
7+
* Deprecated the `Type::getCollectionKeyType()` and `Type::getCollectionValueType()` methods, use `Type::getCollectionKeyTypes()` and `Type::getCollectionValueTypes()` instead.

src/Symfony/Component/PropertyInfo/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
5.3.0
5+
-----
6+
7+
* Added support for multiple types for collection keys & values
8+
* Deprecated the `Type::getCollectionKeyType()` and `Type::getCollectionValueType()` methods, use `Type::getCollectionKeyTypes()` and `Type::getCollectionValueTypes()` instead.
9+
410
5.2.0
511
-----
612

src/Symfony/Component/PropertyInfo/Tests/TypeTest.php

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,24 @@
1212
namespace Symfony\Component\PropertyInfo\Tests;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
1516
use Symfony\Component\PropertyInfo\Type;
1617

1718
/**
1819
* @author Kévin Dunglas <dunglas@gmail.com>
1920
*/
2021
class TypeTest extends TestCase
2122
{
22-
public function testConstruct()
23+
use ExpectDeprecationTrait;
24+
25+
/**
26+
* @group legacy
27+
*/
28+
public function testLegacyConstruct()
2329
{
30+
$this->expectDeprecation('Since symfony/property-info 5.3: The "Symfony\Component\PropertyInfo\Type::getCollectionKeyType()" method is deprecated, use "getCollectionKeyTypes()" instead.');
31+
$this->expectDeprecation('Since symfony/property-info 5.3: The "Symfony\Component\PropertyInfo\Type::getCollectionValueType()" method is deprecated, use "getCollectionValueTypes()" instead.');
32+
2433
$type = new Type('object', true, 'ArrayObject', true, new Type('int'), new Type('string'));
2534

2635
$this->assertEquals(Type::BUILTIN_TYPE_OBJECT, $type->getBuiltinType());
@@ -37,6 +46,26 @@ public function testConstruct()
3746
$this->assertEquals(Type::BUILTIN_TYPE_STRING, $collectionValueType->getBuiltinType());
3847
}
3948

49+
public function testConstruct()
50+
{
51+
$type = new Type('object', true, 'ArrayObject', true, new Type('int'), new Type('string'));
52+
53+
$this->assertEquals(Type::BUILTIN_TYPE_OBJECT, $type->getBuiltinType());
54+
$this->assertTrue($type->isNullable());
55+
$this->assertEquals('ArrayObject', $type->getClassName());
56+
$this->assertTrue($type->isCollection());
57+
58+
$collectionKeyTypes = $type->getCollectionKeyTypes();
59+
$this->assertIsArray($collectionKeyTypes);
60+
$this->assertContainsOnlyInstancesOf('Symfony\Component\PropertyInfo\Type', $collectionKeyTypes);
61+
$this->assertEquals(Type::BUILTIN_TYPE_INT, $collectionKeyTypes[0]->getBuiltinType());
62+
63+
$collectionValueTypes = $type->getCollectionValueTypes();
64+
$this->assertIsArray($collectionValueTypes);
65+
$this->assertContainsOnlyInstancesOf('Symfony\Component\PropertyInfo\Type', $collectionValueTypes);
66+
$this->assertEquals(Type::BUILTIN_TYPE_STRING, $collectionValueTypes[0]->getBuiltinType());
67+
}
68+
4069
public function testIterable()
4170
{
4271
$type = new Type('iterable');
@@ -49,4 +78,46 @@ public function testInvalidType()
4978
$this->expectExceptionMessage('"foo" is not a valid PHP type.');
5079
new Type('foo');
5180
}
81+
82+
public function testArrayCollection()
83+
{
84+
$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)]);
85+
86+
$this->assertEquals(Type::BUILTIN_TYPE_ARRAY, $type->getBuiltinType());
87+
$this->assertFalse($type->isNullable());
88+
$this->assertTrue($type->isCollection());
89+
90+
[$firstKeyType, $secondKeyType] = $type->getCollectionKeyTypes();
91+
$this->assertEquals(Type::BUILTIN_TYPE_INT, $firstKeyType->getBuiltinType());
92+
$this->assertFalse($firstKeyType->isNullable());
93+
$this->assertFalse($firstKeyType->isCollection());
94+
$this->assertEquals(Type::BUILTIN_TYPE_STRING, $secondKeyType->getBuiltinType());
95+
$this->assertFalse($secondKeyType->isNullable());
96+
$this->assertFalse($secondKeyType->isCollection());
97+
98+
[$firstValueType, $secondValueType] = $type->getCollectionValueTypes();
99+
$this->assertEquals(Type::BUILTIN_TYPE_OBJECT, $firstValueType->getBuiltinType());
100+
$this->assertEquals(\ArrayObject::class, $firstValueType->getClassName());
101+
$this->assertFalse($firstValueType->isNullable());
102+
$this->assertTrue($firstValueType->isCollection());
103+
$this->assertEquals(Type::BUILTIN_TYPE_ARRAY, $secondValueType->getBuiltinType());
104+
$this->assertFalse($secondValueType->isNullable());
105+
$this->assertTrue($firstValueType->isCollection());
106+
}
107+
108+
public function testInvalidCollectionArgument()
109+
{
110+
$this->expectException('TypeError');
111+
$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.');
112+
113+
new Type('array', false, null, true, new \stdClass(), [new Type('string')]);
114+
}
115+
116+
public function testInvalidCollectionValueArgument()
117+
{
118+
$this->expectException('TypeError');
119+
$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.');
120+
121+
new Type('array', false, null, true, [new \stdClass()], [new Type('string')]);
122+
}
52123
}

src/Symfony/Component/PropertyInfo/Type.php

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,12 @@ class Type
5757
private $collectionValueType;
5858

5959
/**
60+
* @param Type[]|Type|null $collectionKeyType
61+
* @param Type[]|Type|null $collectionValueType
62+
*
6063
* @throws \InvalidArgumentException
6164
*/
62-
public function __construct(string $builtinType, bool $nullable = false, string $class = null, bool $collection = false, self $collectionKeyType = null, self $collectionValueType = null)
65+
public function __construct(string $builtinType, bool $nullable = false, string $class = null, bool $collection = false, $collectionKeyType = null, $collectionValueType = null)
6366
{
6467
if (!\in_array($builtinType, self::$builtinTypes)) {
6568
throw new \InvalidArgumentException(sprintf('"%s" is not a valid PHP type.', $builtinType));
@@ -69,8 +72,31 @@ public function __construct(string $builtinType, bool $nullable = false, string
6972
$this->nullable = $nullable;
7073
$this->class = $class;
7174
$this->collection = $collection;
72-
$this->collectionKeyType = $collectionKeyType;
73-
$this->collectionValueType = $collectionValueType;
75+
$this->collectionKeyType = $this->validateCollectionArgument($collectionKeyType, 5, '$collectionKeyType') ?? [];
76+
$this->collectionValueType = $this->validateCollectionArgument($collectionValueType, 6, '$collectionValueType') ?? [];
77+
}
78+
79+
private function validateCollectionArgument($collectionArgument, int $argumentIndex, string $argumentName): ?array
80+
{
81+
if (null === $collectionArgument) {
82+
return null;
83+
}
84+
85+
if (!\is_array($collectionArgument) && !$collectionArgument instanceof self) {
86+
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)));
87+
}
88+
89+
if (\is_array($collectionArgument)) {
90+
foreach ($collectionArgument as $type) {
91+
if (!$type instanceof self) {
92+
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)));
93+
}
94+
}
95+
96+
return $collectionArgument;
97+
}
98+
99+
return [$collectionArgument];
74100
}
75101

76102
/**
@@ -107,8 +133,33 @@ public function isCollection(): bool
107133
* Gets collection key type.
108134
*
109135
* Only applicable for a collection type.
136+
*
137+
* @deprecated since Symfony 5.3, use "getCollectionKeyTypes()" instead
110138
*/
111139
public function getCollectionKeyType(): ?self
140+
{
141+
trigger_deprecation('symfony/property-info', '5.3', 'The "%s()" method is deprecated, use "getCollectionKeyTypes()" instead.', __METHOD__);
142+
143+
$type = $this->getCollectionKeyTypes();
144+
if (0 === \count($type)) {
145+
return null;
146+
}
147+
148+
if (\is_array($type)) {
149+
[$type] = $type;
150+
}
151+
152+
return $type;
153+
}
154+
155+
/**
156+
* Gets collection key types.
157+
*
158+
* Only applicable for a collection type.
159+
*
160+
* @return Type[]
161+
*/
162+
public function getCollectionKeyTypes(): array
112163
{
113164
return $this->collectionKeyType;
114165
}
@@ -117,8 +168,33 @@ public function getCollectionKeyType(): ?self
117168
* Gets collection value type.
118169
*
119170
* Only applicable for a collection type.
171+
*
172+
* @deprecated since Symfony 5.3, use "getCollectionValueTypes()" instead
120173
*/
121174
public function getCollectionValueType(): ?self
175+
{
176+
trigger_deprecation('symfony/property-info', '5.3', 'The "%s()" method is deprecated, use "getCollectionValueTypes()" instead.', __METHOD__);
177+
178+
$type = $this->getCollectionValueTypes();
179+
if (0 === \count($type)) {
180+
return null;
181+
}
182+
183+
if (\is_array($type)) {
184+
[$type] = $type;
185+
}
186+
187+
return $type;
188+
}
189+
190+
/**
191+
* Gets collection value types.
192+
*
193+
* Only applicable for a collection type.
194+
*
195+
* @return Type[]
196+
*/
197+
public function getCollectionValueTypes(): array
122198
{
123199
return $this->collectionValueType;
124200
}

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ private function validateAndDenormalize(string $currentClass, string $attribute,
373373
return null;
374374
}
375375

376-
$collectionValueType = $type->isCollection() ? $type->getCollectionValueType() : null;
376+
$collectionValueType = $collectionValueType = $type->isCollection() ? $type->getCollectionValueTypes()[0] ?? null : null;
377377

378378
// Fix a collection that contains the only one element
379379
// This is special to xml format only
@@ -431,18 +431,18 @@ private function validateAndDenormalize(string $currentClass, string $attribute,
431431
$builtinType = Type::BUILTIN_TYPE_OBJECT;
432432
$class = $collectionValueType->getClassName().'[]';
433433

434-
if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
435-
$context['key_type'] = $collectionKeyType;
434+
if (null !== $collectionKeyType = $type->getCollectionKeyTypes()) {
435+
[$context['key_type']] = $collectionKeyType;
436436
}
437-
} elseif ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_ARRAY === $collectionValueType->getBuiltinType()) {
437+
} elseif ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueTypes()) && \count($collectionValueType) > 0 && Type::BUILTIN_TYPE_ARRAY === $collectionValueType[0]->getBuiltinType()) {
438438
// get inner type for any nested array
439-
$innerType = $collectionValueType;
439+
[$innerType] = $collectionValueType;
440440

441441
// note that it will break for any other builtinType
442442
$dimensions = '[]';
443-
while (null !== $innerType->getCollectionValueType() && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) {
443+
while (null !== $innerType->getCollectionValueTypes() && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) {
444444
$dimensions .= '[]';
445-
$innerType = $innerType->getCollectionValueType();
445+
[$innerType] = $innerType->getCollectionValueTypes();
446446
}
447447

448448
if (null !== $innerType->getClassName()) {

src/Symfony/Component/Serializer/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"symfony/http-kernel": "^4.4|^5.0",
3535
"symfony/mime": "^4.4|^5.0",
3636
"symfony/property-access": "^4.4|^5.0",
37-
"symfony/property-info": "^4.4|^5.0",
37+
"symfony/property-info": "^5.3",
3838
"symfony/uid": "^5.1",
3939
"symfony/validator": "^4.4|^5.0",
4040
"symfony/var-exporter": "^4.4|^5.0",

src/Symfony/Component/Validator/Mapping/Loader/PropertyInfoLoader.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ public function loadClassMetadata(ClassMetadata $metadata): bool
119119
}
120120
if (!$hasTypeConstraint) {
121121
if (1 === \count($builtinTypes)) {
122-
if ($types[0]->isCollection() && (null !== $collectionValueType = $types[0]->getCollectionValueType())) {
122+
if ($types[0]->isCollection() && (null !== $collectionValueType = $types[0]->getCollectionValueTypes())) {
123+
[$collectionValueType] = $collectionValueType;
123124
$this->handleAllConstraint($property, $allConstraint, $collectionValueType, $metadata);
124125
}
125126

src/Symfony/Component/Validator/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"symfony/cache": "^4.4|^5.0",
3939
"symfony/mime": "^4.4|^5.0",
4040
"symfony/property-access": "^4.4|^5.0",
41-
"symfony/property-info": "^4.4|^5.0",
41+
"symfony/property-info": "^5.3",
4242
"symfony/translation": "^4.4|^5.0",
4343
"doctrine/annotations": "~1.7",
4444
"doctrine/cache": "~1.0",

0 commit comments

Comments
 (0)
0