diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index 51e95c4f32ca4..a0d635c87e2d1 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -1142,7 +1142,7 @@ public function offsetGet(mixed $option, bool $triggerDeprecation = true): mixed private function verifyTypes(string $type, mixed $value, ?array &$invalidTypes = null, int $level = 0): bool { $type = trim($type); - $allowedTypes = $this->splitOutsideParenthesis($type); + $allowedTypes = $this->splitTypes($type); if (\count($allowedTypes) > 1) { foreach ($allowedTypes as $allowedType) { if ($this->verifyTypes($allowedType, $value)) { @@ -1162,17 +1162,29 @@ private function verifyTypes(string $type, mixed $value, ?array &$invalidTypes = return $this->verifyTypes(substr($type, 1, -1), $value, $invalidTypes, $level); } - if (\is_array($value) && str_ends_with($type, '[]')) { - $type = substr($type, 0, -2); - $valid = true; + if (\is_array($value)) { + if (str_starts_with($type, 'array<') && str_ends_with($type, '>')) { + $types = $this->splitTypes(substr($type, 6, -1), ','); + $allowedValueType = $types[1] ?? $types[0]; + $allowedKeyType = isset($types[1]) ? $types[0] : null; + } elseif (str_ends_with($type, '[]')) { + $allowedValueType = substr($type, 0, -2); + } + + if (isset($allowedValueType)) { + $valid = true; + foreach ($value as $key => $val) { + if (isset($allowedKeyType) && !$this->verifyTypes($allowedKeyType, $key, $invalidTypes, $level + 1)) { + $valid = false; + } - foreach ($value as $val) { - if (!$this->verifyTypes($type, $val, $invalidTypes, $level + 1)) { - $valid = false; + if (!$this->verifyTypes($allowedValueType, $val, $invalidTypes, $level + 1)) { + $valid = false; + } } - } - return $valid; + return $valid; + } } if (('null' === $type && null === $value) || (isset(self::VALIDATION_FUNCTIONS[$type]) ? self::VALIDATION_FUNCTIONS[$type]($value) : $value instanceof $type)) { @@ -1189,27 +1201,31 @@ private function verifyTypes(string $type, mixed $value, ?array &$invalidTypes = /** * @return list */ - private function splitOutsideParenthesis(string $type): array + private function splitTypes(string $type, string $separator = '|'): array { $parts = []; $currentPart = ''; $parenthesisLevel = 0; + $arrayLevel = 0; $typeLength = \strlen($type); for ($i = 0; $i < $typeLength; ++$i) { $char = $type[$i]; + if ($separator === $char && 0 === $parenthesisLevel && 0 === $arrayLevel) { + $parts[] = $currentPart; + $currentPart = ''; + continue; + } + $currentPart .= $char; if ('(' === $char) { ++$parenthesisLevel; } elseif (')' === $char) { --$parenthesisLevel; - } - - if ('|' === $char && 0 === $parenthesisLevel) { - $parts[] = $currentPart; - $currentPart = ''; - } else { - $currentPart .= $char; + } elseif ('<' === $char) { + ++$arrayLevel; + } elseif ('>' === $char) { + --$arrayLevel; } } diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 96faa09b07d83..1307b10f0fe12 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -828,6 +828,18 @@ public function testResolveTypedWithUnionOfArray() $this->assertSame(['foo' => [1, true]], $options); } + public function testResolveTypedWithUnionOfArray2() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'array|array'); + + $options = $this->resolver->resolve(['foo' => [1, '1']]); + $this->assertSame(['foo' => [1, '1']], $options); + + $options = $this->resolver->resolve(['foo' => [1, true]]); + $this->assertSame(['foo' => [1, true]], $options); + } + public function testResolveTypedArray() { $this->resolver->setDefined('foo'); @@ -837,6 +849,42 @@ public function testResolveTypedArray() $this->assertSame(['foo' => ['bar', 'baz']], $options); } + public function testResolveTypedArray2() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'array'); + $options = $this->resolver->resolve(['foo' => ['bar', 'baz']]); + + $this->assertSame(['foo' => ['bar', 'baz']], $options); + } + + public function testResolveTypedArrayWithKeys() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'array'); + $options = $this->resolver->resolve(['foo' => ['bar', 'baz']]); + + $this->assertSame(['foo' => ['bar', 'baz']], $options); + } + + public function testResolveTypedArrayWithStringKeys() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'array'); + $options = $this->resolver->resolve(['foo' => ['bar' => 'bar', 'baz' => 'baz']]); + + $this->assertSame(['foo' => ['bar' => 'bar', 'baz' => 'baz']], $options); + } + + public function testResolveTypedArrayWithInvalidKeys() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'array'); + + $this->expectException(InvalidOptionsException::class); + $this->resolver->resolve(['foo' => ['bar', 'baz']]); + } + public function testResolveTypedArrayWithUnion() { $this->resolver->setDefined('foo'); @@ -846,6 +894,15 @@ public function testResolveTypedArrayWithUnion() $this->assertSame(['foo' => ['bar', 1]], $options); } + public function testResolveTypedArrayWithUnion2() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'array'); + $options = $this->resolver->resolve(['foo' => ['bar', 1]]); + + $this->assertSame(['foo' => ['bar', 1]], $options); + } + public function testFailIfSetAllowedTypesFromLazyOption() { $this->expectException(AccessException::class); @@ -1963,6 +2020,24 @@ public function testNestedArrays() ])); } + public function testNestedArrays2() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'array>'); + + $this->assertSame([ + 'foo' => [ + [ + 1, 2, + ], + ], + ], $this->resolver->resolve([ + 'foo' => [ + [1, 2], + ], + ])); + } + public function testNestedArraysWithUnions() { $this->resolver->setDefined('foo'); @@ -1983,6 +2058,26 @@ public function testNestedArraysWithUnions() ])); } + public function testNestedArraysWithUnions2() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'array>'); + + $this->assertEquals([ + 'foo' => [ + 1, + 2.0, + [1, 2.0], + ], + ], $this->resolver->resolve([ + 'foo' => [ + 1, + 2.0, + [1, 2.0], + ], + ])); + } + public function testNested2Arrays() { $this->resolver->setDefined('foo');