Skip to content

Conversation

VincentLanglet
Copy link
Contributor

@VincentLanglet VincentLanglet commented Jan 3, 2025

Q A
Branch? 7.3
Bug fix? no
New feature? yes
Deprecations? no
Issues Fix #...
License MIT

Add support for | syntax when describing allowedType in OptionResolver.

It's not really useful for int|string since we can pass an array ['string', 'int'].
But it's useful for array values since (int|string)[] was not possible so far and ['string[]', 'int[]'] was not the same thing.

@carsonbot
Copy link

Hey!

Thanks for your PR. You are targeting branch "7.3" but it seems your PR description refers to branch "7.3 for features".
Could you update the PR description or change target branch? This helps core maintainers a lot.

Cheers!

Carsonbot

Copy link
Member

@yceruto yceruto left a comment

Choose a reason for hiding this comment

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

Good addition 👍 thanks!

@yceruto
Copy link
Member

yceruto commented Jan 3, 2025

Also, we could find a regex that matches all possible variants instead of checking them one by one.

@VincentLanglet
Copy link
Contributor Author

VincentLanglet commented Jan 3, 2025

Also, we could find a regex that matches all possible variants instead of checking them one by one.

Didn't find an easy one, but I'm opened to suggestion.

I preferred to add the logic in the existing recursive implementation

@VincentLanglet
Copy link
Contributor Author

I found edge case where my previous implementation remove badly parenthesis, so I now introduce a private method isEnclosedWithParenthesis @yceruto

@yceruto
Copy link
Member

yceruto commented Jan 3, 2025

Could you confirm that class Union is also supported with this new syntax? i.e. (App\FooInterface|App\BarInterface)[]

@VincentLanglet
Copy link
Contributor Author

Could you confirm that class Union is also supported with this new syntax? i.e. (App\FooInterface|App\BarInterface)[]

I added a test for \DateTime::class.'|'.\DateTimeImmutable::class. Is it enough ?

@fabpot
Copy link
Member

fabpot commented Jan 6, 2025

Thank you @VincentLanglet.

@fabpot fabpot merged commit cd24b4b into symfony:7.3 Jan 6, 2025
9 of 11 checks passed
fabpot added a commit that referenced this pull request Jan 12, 2025
…hangelog (alamirault)

This PR was merged into the 7.3 branch.

Discussion
----------

[OptionsResolver] Add missing support of union type in changelog

| Q             | A
| ------------- | ---
| Branch?       | 7.3
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Issues        | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exists, explain below instead -->
| License       | MIT

I found changelog is missing while documenting symfony/symfony-docs#20531

Code PR: #59354

Commits
-------

946278f [OptionsResolver] Add missing support of union type
@fabpot fabpot mentioned this pull request May 2, 2025
nicolas-grekas added a commit that referenced this pull request Jul 29, 2025
….9x faster (bendavies)

This PR was squashed before being merged into the 7.4 branch.

Discussion
----------

[OptionsResolver] Optimize splitOutsideParenthesis() - 5.9x faster

| Q             | A
| ------------- | ---
| Branch?       | 7.4
| Bug fix?      | no
| New feature?  | no <!-- if yes, also update src/**/CHANGELOG.md -->
| Deprecations? | no <!-- if yes, also update UPGRADE-*.md and src/**/CHANGELOG.md -->
| Issues        | Fix #59354 <!-- prefix each issue number with "Fix #"; no need to create an issue if none exists, explain below -->
| License       | MIT

This PR optimises the `splitOutsideParenthesis` method in `OptionsResolver.php`, achieving a 2.91x performance improvement.

I discovered this method as a performance hotspot while benchmarking a large Symfony form with many fields. Profiling revealed that `splitOutsideParenthesis` was consuming a significant portion of the form processing time.

The `splitOutsideParenthesis` method (introduced in [PR #59354](#59354)) is called frequently during options resolution and has several performance bottlenecks:

1. Character-by-character string concatenation creates new string objects on each iteration (particularly inefficient in PHP due to copy-on-write behavior)
2. All input strings are processed the same way, regardless of complexity - no fast path for simple types
3. Multiple conditional checks per character

## Test Methodology

Here's how all performance measurements were conducted:

- **Benchmark tool**: hyperfine (10 runs with 1 warmup run)
- **Test iterations**: 100,000 iterations per test case
- **Test data**: 16 different type patterns:
    - Simple types: `string`, `int`, `bool`, `array`
    - Union types: `string|int`, `string|int|bool`, `string|int|bool|array`
    - Parentheses types: `string|(int|bool)`, `(string|int)|bool`
    - Nested types: `string|(int|(bool|float))`, `(string|int)|(bool|float)`
    - Array types: `string[]`, `int[]`
    - Class types: `MyClass`, `\\Namespace\\Class`
    - Complex union: `string|int|bool|array|object|resource|callable`

Each optimisation was tested in isolation to measure its individual impact, then all optimisations were combined for the final benchmark.

## Optimisations

### 1. Fast Path for Simple Types (No Pipes)

Most type declarations are simple types like `string`, `int`, `MyClass`, etc. without any union types.

**Implementation:**
```php
if (!\str_contains($type, '|')) {
    return [$type];
}
```

### 2. Fast Path for Union Types (No Parentheses)

Common union types like `string|int|bool` don't need complex parsing - PHP's `explode()` is much faster.

**Implementation:**
```php
if (!\str_contains($type, '(') && !\str_contains($type, ')')) {
    return \explode('|', $type);
}
```

### 3. Eliminate String Concatenation

String concatenation in loops creates memory overhead. Using `substr()` avoids creating intermediate strings.

**Implementation:**
```php
// Instead of: $currentPart .= $char;
// Use: $parts[] = \substr($type, $start, $i - $start);
```

### 4. Switch Statement Optimisation

Eliminates Multiple conditional checks per character.

**Implementation:**
```php
switch ($char) {
    case '(':
        ++$parenthesisLevel;
        break;
    case ')':
        --$parenthesisLevel;
        break;
    case '|':
        // ...
}
```

## Benchmark Results

### Individual Optimisation Impact

Testing each optimisation in isolation:

```bash
hyperfine --warmup 1 --runs 10 \
  --sort=command \
  --reference 'php test_original.php' \
  'php test_opt1_fast_path_simple.php' \
  'php test_opt2_fast_path_union.php' \
  'php test_opt3_no_string_concat.php'  \
  'php test_opt4_switch_statement.php'
```

```
Relative speed comparison
        1.00          php test_original.php
        1.23 ±  0.02  php test_opt1_fast_path_simple.php
        1.95 ±  0.04  php test_opt2_fast_path_union.php
        1.13 ±  0.03  php test_opt3_no_string_concat.php
        1.35 ±  0.03  php test_opt4_switch_statement.php
```

### Combined Optimisation Impact

Combining all optimisations:

```bash
hyperfine --warmup 1 --runs 10 \
  --sort=command \
  --reference 'php test_original.php' \
  'php test_optimised.php'
```
```
Relative speed comparison
        1.00          php test_original.php
        2.91 ±  0.03  php test_optimised.php
```

Commits
-------

b568fef [OptionsResolver] Optimize splitOutsideParenthesis() - 5.9x faster
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants