Skip to content

Commit 29c7bfa

Browse files
committed
feature #39020 [PropertyInfo] Support multiple types for collection keys & values (Korbeil)
This PR was merged into the 5.3-dev branch. Discussion ---------- [PropertyInfo] Support multiple types for collection keys & values | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | no | New feature? | yes | Deprecations? | yes | Tickets | #38093 | License | MIT | Doc PR | N/A This PR is here to introduce multiple types for collection keys & values. Today, we support types as following: `A|B|C` thanks to `getTypes` interface (in extractors) but we do not support union types in collection keys or values, such as `array<A|B|C>`. This PR will allow this. In a next PR, we'll introduce an Extractor that will parse phpDoc in order to have union types in collection keys or values. I tried to introduce as few depreciations as possible, we have only 2 of them here: - `Type::getCollectionKeyType` - `Type::getCollectionValueType` Commits ------- 84dd178 Support multiple types for collection keys & values
2 parents e1e1def + 84dd178 commit 29c7bfa

File tree

8 files changed

+173
-14
lines changed

8 files changed

+173
-14
lines changed

UPGRADE-5.3.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ PhpunitBridge
3333

3434
* Deprecated the `SetUpTearDownTrait` trait, use original methods with "void" return typehint.
3535

36+
PropertyInfo
37+
------------
38+
39+
* Deprecated the `Type::getCollectionKeyType()` and `Type::getCollectionValueType()` methods, use `Type::getCollectionKeyTypes()` and `Type::getCollectionValueTypes()` instead.
40+
3641
Security
3742
--------
3843

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 = $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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy