Skip to content

Commit 8726f36

Browse files
Support union of types in OptionsResolver
1 parent 04ee771 commit 8726f36

File tree

2 files changed

+78
-3
lines changed

2 files changed

+78
-3
lines changed

src/Symfony/Component/OptionsResolver/OptionsResolver.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,8 +1139,19 @@ public function offsetGet(mixed $option, bool $triggerDeprecation = true): mixed
11391139
return $value;
11401140
}
11411141

1142-
private function verifyTypes(string $type, mixed $value, array &$invalidTypes, int $level = 0): bool
1142+
private function verifyTypes(string $type, mixed $value, ?array &$invalidTypes = null, int $level = 0): bool
11431143
{
1144+
if (str_starts_with($type, '(') && str_ends_with($type, ')')) {
1145+
$substr = substr($type, 1, -1);
1146+
1147+
// Ensure we didn't simplify `(A)|(B)` into `A)|(B`
1148+
$openingPos = strpos($substr, '(');
1149+
$closingPos = strpos($substr, ')');
1150+
if (false === $closingPos || $closingPos > $openingPos) {
1151+
return $this->verifyTypes($substr, $value, $invalidTypes, $level);
1152+
}
1153+
}
1154+
11441155
if (\is_array($value) && str_ends_with($type, '[]')) {
11451156
$type = substr($type, 0, -2);
11461157
$valid = true;
@@ -1154,11 +1165,18 @@ private function verifyTypes(string $type, mixed $value, array &$invalidTypes, i
11541165
return $valid;
11551166
}
11561167

1157-
if (('null' === $type && null === $value) || (isset(self::VALIDATION_FUNCTIONS[$type]) ? self::VALIDATION_FUNCTIONS[$type]($value) : $value instanceof $type)) {
1168+
if (str_contains($type, '|')) {
1169+
$allowedTypes = preg_split('/\|(?![^\(]*\))/', $type);
1170+
foreach ($allowedTypes as $allowedType) {
1171+
if ($this->verifyTypes($allowedType, $value)) {
1172+
return true;
1173+
}
1174+
}
1175+
} elseif (('null' === $type && null === $value) || (isset(self::VALIDATION_FUNCTIONS[$type]) ? self::VALIDATION_FUNCTIONS[$type]($value) : $value instanceof $type)) {
11581176
return true;
11591177
}
11601178

1161-
if (!$invalidTypes || $level > 0) {
1179+
if (\is_array($invalidTypes) && (!$invalidTypes || $level > 0)) {
11621180
$invalidTypes[get_debug_type($value)] = true;
11631181
}
11641182

src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,33 @@ public function testSetAllowedTypesFailsIfUnknownOption()
778778
$this->resolver->setAllowedTypes('foo', 'string');
779779
}
780780

781+
public function testResolveTypedWithUnion()
782+
{
783+
$this->resolver->setDefined('foo');
784+
$this->resolver->setAllowedTypes('foo', 'string|int');
785+
$options = $this->resolver->resolve(['foo' => 1]);
786+
787+
$this->assertSame(['foo' => 1], $options);
788+
}
789+
790+
public function testResolveTypedWithUnionUselessParentheses()
791+
{
792+
$this->resolver->setDefined('foo');
793+
$this->resolver->setAllowedTypes('foo', '(string)|(int)');
794+
$options = $this->resolver->resolve(['foo' => 1]);
795+
796+
$this->assertSame(['foo' => 1], $options);
797+
}
798+
799+
public function testResolveTypedWithUnionUselessParentheses2()
800+
{
801+
$this->resolver->setDefined('foo');
802+
$this->resolver->setAllowedTypes('foo', '((string)|(int))');
803+
$options = $this->resolver->resolve(['foo' => 1]);
804+
805+
$this->assertSame(['foo' => 1], $options);
806+
}
807+
781808
public function testResolveTypedArray()
782809
{
783810
$this->resolver->setDefined('foo');
@@ -787,6 +814,15 @@ public function testResolveTypedArray()
787814
$this->assertSame(['foo' => ['bar', 'baz']], $options);
788815
}
789816

817+
public function testResolveTypedArrayWithUnion()
818+
{
819+
$this->resolver->setDefined('foo');
820+
$this->resolver->setAllowedTypes('foo', '(string|int)[]');
821+
$options = $this->resolver->resolve(['foo' => ['bar', 1]]);
822+
823+
$this->assertSame(['foo' => ['bar', 1]], $options);
824+
}
825+
790826
public function testFailIfSetAllowedTypesFromLazyOption()
791827
{
792828
$this->expectException(AccessException::class);
@@ -878,6 +914,7 @@ public static function provideInvalidTypes()
878914
[[null], ['string[]', 'string'], 'The option "option" with value array is expected to be of type "string[]" or "string", but one of the elements is of type "null".'],
879915
[['string', null], ['string[]', 'string'], 'The option "option" with value array is expected to be of type "string[]" or "string", but one of the elements is of type "null".'],
880916
[[\stdClass::class], ['string'], 'The option "option" with value array is expected to be of type "string", but is of type "array".'],
917+
[['foo', 12], '(string|bool)[]', 'The option "option" with value array is expected to be of type "(string|bool)[]", but one of the elements is of type "int".'],
881918
];
882919
}
883920

@@ -1903,6 +1940,26 @@ public function testNestedArrays()
19031940
]));
19041941
}
19051942

1943+
public function testNestedArraysWithUnions()
1944+
{
1945+
$this->resolver->setDefined('foo');
1946+
$this->resolver->setAllowedTypes('foo', '(int|float|(int|float)[])[]');
1947+
1948+
$this->assertEquals([
1949+
'foo' => [
1950+
1,
1951+
2.0,
1952+
[1, 2.0],
1953+
],
1954+
], $this->resolver->resolve([
1955+
'foo' => [
1956+
1,
1957+
2.0,
1958+
[1, 2.0],
1959+
],
1960+
]));
1961+
}
1962+
19061963
public function testNested2Arrays()
19071964
{
19081965
$this->resolver->setDefined('foo');

0 commit comments

Comments
 (0)