Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 23 additions & 27 deletions src/Symfony/Component/OptionsResolver/OptionsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1215,33 +1215,29 @@ private function verifyTypes(string $type, mixed $value, ?array &$invalidTypes =
*/
private function splitOutsideParenthesis(string $type): array
{
$parts = [];
$currentPart = '';
$parenthesisLevel = 0;

$typeLength = \strlen($type);
for ($i = 0; $i < $typeLength; ++$i) {
$char = $type[$i];

if ('(' === $char) {
++$parenthesisLevel;
} elseif (')' === $char) {
--$parenthesisLevel;
}

if ('|' === $char && 0 === $parenthesisLevel) {
$parts[] = $currentPart;
$currentPart = '';
} else {
$currentPart .= $char;
}
}

if ('' !== $currentPart) {
$parts[] = $currentPart;
}

return $parts;
return preg_split(<<<'EOF'
/
# Define a recursive subroutine for matching balanced parentheses
(?(DEFINE)
(?<balanced>
\( # Match an opening parenthesis
(?: # Start a non-capturing group for the contents
[^()] # Match any character that is not a parenthesis
| # OR
(?&balanced) # Recursively match a nested balanced group
)* # Repeat the group for all contents
\) # Match the final closing parenthesis
)
)

# Match any balanced parenthetical group, then skip it
(?&balanced)(*SKIP)(*FAIL) # Use the defined subroutine and discard the match

| # OR

\| # Match the pipe delimiter (only if not inside a skipped group)
/x
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you try using a recursive regexp? this will remove the current limitation of parsing only the first 2 levels of nesting:

/
  # Match a recursively balanced parenthetical group, then skip it
  \(              # Match an opening parenthesis
  (?:             # Start a non-capturing group for the contents
      [^()]       # Match any character that is not a parenthesis
    |             # OR
      (?R)        # Recurse the entire pattern to match a nested group
  )*              # Repeat the group for all contents
  \)              # Match the final closing parenthesis
  (*SKIP)(*FAIL)  # Discard the match and find the next one
| # OR
  \|              # Match the pipe delimiter (only if not inside a skipped group)
/x

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this didnt work because i think (?R) is trying to match the entire pattern (including the (*SKIP)(*FAIL) part i think?

fixed it by defining a recursive subroutine.

EOF, $type);
}

/**
Expand Down
139 changes: 139 additions & 0 deletions src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,145 @@ public function testNestedArraysException()
]);
}

/**
* @dataProvider provideValidDeeplyNestedUnionTypes
*/
public function testDeeplyNestedUnionTypes(string $type, $validValue)
{
$this->resolver->setDefined('option');
$this->resolver->setAllowedTypes('option', $type);
$this->assertEquals(['option' => $validValue], $this->resolver->resolve(['option' => $validValue]));
}

/**
* @dataProvider provideInvalidDeeplyNestedUnionTypes
*/
public function testDeeplyNestedUnionTypesException(string $type, $invalidValue, string $expectedExceptionMessage)
{
$this->resolver->setDefined('option');
$this->resolver->setAllowedTypes('option', $type);

$this->expectException(InvalidOptionsException::class);
$this->expectExceptionMessage($expectedExceptionMessage);

$this->resolver->resolve(['option' => $invalidValue]);
}

public function provideValidDeeplyNestedUnionTypes(): array
{
$resource = fopen('php://memory', 'r');
$object = new \stdClass();

return [
// Test 1 level of nesting
['string|(int|bool)', 'test'],
['string|(int|bool)', 42],
['string|(int|bool)', true],

// Test 2 levels of nesting
['string|(int|(bool|float))', 'test'],
['string|(int|(bool|float))', 42],
['string|(int|(bool|float))', true],
['string|(int|(bool|float))', 3.14],

// Test 3 levels of nesting
['string|(int|(bool|(float|null)))', 'test'],
['string|(int|(bool|(float|null)))', 42],
['string|(int|(bool|(float|null)))', true],
['string|(int|(bool|(float|null)))', 3.14],
['string|(int|(bool|(float|null)))', null],

// Test 4 levels of nesting
['string|(int|(bool|(float|(null|object))))', 'test'],
['string|(int|(bool|(float|(null|object))))', 42],
['string|(int|(bool|(float|(null|object))))', true],
['string|(int|(bool|(float|(null|object))))', 3.14],
['string|(int|(bool|(float|(null|object))))', null],
['string|(int|(bool|(float|(null|object))))', $object],

// Test complex case with multiple deep nesting
['(string|(int|bool))|(float|(null|object))', 'test'],
['(string|(int|bool))|(float|(null|object))', 42],
['(string|(int|bool))|(float|(null|object))', true],
['(string|(int|bool))|(float|(null|object))', 3.14],
['(string|(int|bool))|(float|(null|object))', null],
['(string|(int|bool))|(float|(null|object))', $object],

// Test nested at the beginning
['((string|int)|bool)|float', 'test'],
['((string|int)|bool)|float', 42],
['((string|int)|bool)|float', true],
['((string|int)|bool)|float', 3.14],

// Test multiple unions at different levels
['string|(int|(bool|float))|null|(object|(array|resource))', 'test'],
['string|(int|(bool|float))|null|(object|(array|resource))', 42],
['string|(int|(bool|float))|null|(object|(array|resource))', true],
['string|(int|(bool|float))|null|(object|(array|resource))', 3.14],
['string|(int|(bool|float))|null|(object|(array|resource))', null],
['string|(int|(bool|float))|null|(object|(array|resource))', $object],
['string|(int|(bool|float))|null|(object|(array|resource))', []],
['string|(int|(bool|float))|null|(object|(array|resource))', $resource],

// Test arrays with nested union types:
['(string|int)[]|(bool|float)[]', ['test', 42]],
['(string|int)[]|(bool|float)[]', [true, 3.14]],

// Test deeply nested arrays with unions
['((string|int)|(bool|float))[]', ['test', 42, true, 3.14]],

// Test complex nested array types
['(string|(int|bool)[])|(float|(null|object)[])', 'test'],
['(string|(int|bool)[])|(float|(null|object)[])', [42, true]],
['(string|(int|bool)[])|(float|(null|object)[])', 3.14],
['(string|(int|bool)[])|(float|(null|object)[])', [null, $object]],

// Test multi-dimensional arrays with nesting
['((string|int)[]|(bool|float)[])|null', ['test', 42]],
['((string|int)[]|(bool|float)[])|null', [true, 3.14]],
['((string|int)[]|(bool|float)[])|null', null],
];
}

public function provideInvalidDeeplyNestedUnionTypes(): array
{
$resource = fopen('php://memory', 'r');
$object = new \stdClass();

return [
// Test 1 level of nesting
['string|(int|bool)', [], 'The option "option" with value array is expected to be of type "string|(int|bool)", but is of type "array".'],
['string|(int|bool)', $object, 'The option "option" with value stdClass is expected to be of type "string|(int|bool)", but is of type "stdClass".'],
['string|(int|bool)', $resource, 'The option "option" with value resource is expected to be of type "string|(int|bool)", but is of type "resource (stream)".'],
['string|(int|bool)', null, 'The option "option" with value null is expected to be of type "string|(int|bool)", but is of type "null".'],
['string|(int|bool)', 3.14, 'The option "option" with value 3.14 is expected to be of type "string|(int|bool)", but is of type "float".'],

// Test 2 levels of nesting
['string|(int|(bool|float))', [], 'The option "option" with value array is expected to be of type "string|(int|(bool|float))", but is of type "array".'],
['string|(int|(bool|float))', $object, 'The option "option" with value stdClass is expected to be of type "string|(int|(bool|float))", but is of type "stdClass".'],
['string|(int|(bool|float))', $resource, 'The option "option" with value resource is expected to be of type "string|(int|(bool|float))", but is of type "resource (stream)".'],
['string|(int|(bool|float))', null, 'The option "option" with value null is expected to be of type "string|(int|(bool|float))", but is of type "null".'],

// Test 3 levels of nesting
['string|(int|(bool|(float|null)))', [], 'The option "option" with value array is expected to be of type "string|(int|(bool|(float|null)))", but is of type "array".'],
['string|(int|(bool|(float|null)))', $object, 'The option "option" with value stdClass is expected to be of type "string|(int|(bool|(float|null)))", but is of type "stdClass".'],
['string|(int|(bool|(float|null)))', $resource, 'The option "option" with value resource is expected to be of type "string|(int|(bool|(float|null)))", but is of type "resource (stream)".'],

// Test arrays with nested union types
['(string|int)[]|(bool|float)[]', ['test', true], 'The option "option" with value array is expected to be of type "(string|int)[]|(bool|float)[]", but one of the elements is of type "array".'],
['(string|int)[]|(bool|float)[]', [42, 3.14], 'The option "option" with value array is expected to be of type "(string|int)[]|(bool|float)[]", but one of the elements is of type "array".'],

// Test deeply nested arrays with unions
['((string|int)|(bool|float))[]', 'test', 'The option "option" with value "test" is expected to be of type "((string|int)|(bool|float))[]", but is of type "string".'],
['((string|int)|(bool|float))[]', [null], 'The option "option" with value array is expected to be of type "((string|int)|(bool|float))[]", but one of the elements is of type "null".'],
['((string|int)|(bool|float))[]', [$object], 'The option "option" with value array is expected to be of type "((string|int)|(bool|float))[]", but one of the elements is of type "stdClass".'],

// Test complex nested array types
['(string|(int|bool)[])|(float|(null|object)[])', ['test'], 'The option "option" with value array is expected to be of type "(string|(int|bool)[])|(float|(null|object)[])", but is of type "array".'],
['(string|(int|bool)[])|(float|(null|object)[])', [3.14], 'The option "option" with value array is expected to be of type "(string|(int|bool)[])|(float|(null|object)[])", but is of type "array".'],
];
}

public function testNestedArrayException1()
{
$this->expectException(InvalidOptionsException::class);
Expand Down
Loading