diff --git a/UPGRADE-7.3.md b/UPGRADE-7.3.md index 043791eee00c9..c4fff7bd2301c 100644 --- a/UPGRADE-7.3.md +++ b/UPGRADE-7.3.md @@ -94,6 +94,25 @@ FrameworkBundle * Deprecate the `--show-arguments` option of the `container:debug` command, as arguments are now always shown * Deprecate the `framework.validation.cache` config option +OptionsResolver +--------------- + +* Deprecate defining nested options via `setDefault()`, use `setOptions()` instead + + *Before* + ```php + $resolver->setDefault('option', function (OptionsResolver $resolver) { + // ... + }); + ``` + + *After* + ```php + $resolver->setOptions('option', function (OptionsResolver $resolver) { + // ... + }); + ``` + SecurityBundle -------------- diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php index 9ec6d3ad567ac..d3dd9085c834d 100644 --- a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php @@ -170,7 +170,7 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('debug', 'bool'); $resolver->setDefault('referrals', false); $resolver->setAllowedTypes('referrals', 'bool'); - $resolver->setDefault('options', function (OptionsResolver $options, Options $parent) { + $resolver->setOptions('options', function (OptionsResolver $options, Options $parent) { $options->setDefined(array_map('strtolower', array_keys((new \ReflectionClass(ConnectionOptions::class))->getConstants()))); if (true === $parent['debug']) { diff --git a/src/Symfony/Component/Ldap/composer.json b/src/Symfony/Component/Ldap/composer.json index 5ed2995736e11..535ad186207ec 100644 --- a/src/Symfony/Component/Ldap/composer.json +++ b/src/Symfony/Component/Ldap/composer.json @@ -18,14 +18,13 @@ "require": { "php": ">=8.2", "ext-ldap": "*", - "symfony/options-resolver": "^6.4|^7.0" + "symfony/options-resolver": "^7.3" }, "require-dev": { "symfony/security-core": "^6.4|^7.0", "symfony/security-http": "^6.4|^7.0" }, "conflict": { - "symfony/options-resolver": "<6.4", "symfony/security-core": "<6.4" }, "autoload": { diff --git a/src/Symfony/Component/OptionsResolver/CHANGELOG.md b/src/Symfony/Component/OptionsResolver/CHANGELOG.md index 3e774eec10fe4..5bdc9e3f5863f 100644 --- a/src/Symfony/Component/OptionsResolver/CHANGELOG.md +++ b/src/Symfony/Component/OptionsResolver/CHANGELOG.md @@ -5,6 +5,8 @@ CHANGELOG --- * Support union type in `OptionResolver::setAllowedTypes()` method + * Add `OptionsResolver::setOptions()` and `OptionConfigurator::options()` methods + * Deprecate defining nested options via `setDefault()`, use `setOptions()` instead 6.4 --- diff --git a/src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php b/src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php index dab741b426b77..304a1a4aeccea 100644 --- a/src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php +++ b/src/Symfony/Component/OptionsResolver/Debug/OptionsResolverIntrospector.php @@ -101,4 +101,14 @@ public function getDeprecation(string $option): array { return ($this->get)('deprecated', $option, \sprintf('No deprecation was set for the "%s" option.', $option)); } + + /** + * @return \Closure[] + * + * @throws NoConfigurationException when no nested option is configured + */ + public function getNestedOptions(string $option): array + { + return ($this->get)('nested', $option, \sprintf('No nested option was set for the "%s" option.', $option)); + } } diff --git a/src/Symfony/Component/OptionsResolver/OptionConfigurator.php b/src/Symfony/Component/OptionsResolver/OptionConfigurator.php index e708c2cefb386..891b26587a331 100644 --- a/src/Symfony/Component/OptionsResolver/OptionConfigurator.php +++ b/src/Symfony/Component/OptionsResolver/OptionConfigurator.php @@ -143,4 +143,18 @@ public function ignoreUndefined(bool $ignore = true): static return $this; } + + /** + * Defines nested options. + * + * @param \Closure(OptionsResolver $resolver, Options $parent): void $nested + * + * @return $this + */ + public function options(\Closure $nested): static + { + $this->resolver->setOptions($this->name, $nested); + + return $this; + } } diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index 51e95c4f32ca4..82ca9166ee09d 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -64,6 +64,13 @@ class OptionsResolver implements Options */ private array $nested = []; + /** + * BC layer. Remove in Symfony 8.0. + * + * @var array + */ + private array $deprecatedNestedOptions = []; + /** * The names of required options. */ @@ -178,20 +185,6 @@ class OptionsResolver implements Options * is spread across different locations of your code, such as base and * sub-classes. * - * If you want to define nested options, you can pass a closure with the - * following signature: - * - * $options->setDefault('database', function (OptionsResolver $resolver) { - * $resolver->setDefined(['dbname', 'host', 'port', 'user', 'pass']); - * } - * - * To get access to the parent options, add a second argument to the closure's - * signature: - * - * function (OptionsResolver $resolver, Options $parent) { - * // 'default' === $parent['connection'] - * } - * * @return $this * * @throws AccessException If called from a lazy option or normalizer @@ -226,13 +219,22 @@ public function setDefault(string $option, mixed $value): static $this->lazy[$option][] = $value; $this->defined[$option] = true; - // Make sure the option is processed and is not nested anymore - unset($this->resolved[$option], $this->nested[$option]); + // Make sure the option is processed + unset($this->resolved[$option]); + + // BC layer. Remove in Symfony 8.0. + if (isset($this->deprecatedNestedOptions[$option])) { + unset($this->nested[$option]); + } return $this; } + // Remove in Symfony 8.0. if (isset($params[0]) && ($type = $params[0]->getType()) instanceof \ReflectionNamedType && self::class === $type->getName() && (!isset($params[1]) || (($type = $params[1]->getType()) instanceof \ReflectionNamedType && Options::class === $type->getName()))) { + trigger_deprecation('symfony/options-resolver', '7.3', 'Defining nested options via "%s()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.', __METHOD__); + $this->deprecatedNestedOptions[$option] = true; + // Store closure for later evaluation $this->nested[$option][] = $value; $this->defaults[$option] = []; @@ -245,8 +247,13 @@ public function setDefault(string $option, mixed $value): static } } - // This option is not lazy nor nested anymore - unset($this->lazy[$option], $this->nested[$option]); + // This option is not lazy anymore + unset($this->lazy[$option]); + + // BC layer. Remove in Symfony 8.0. + if (isset($this->deprecatedNestedOptions[$option])) { + unset($this->nested[$option]); + } // Yet undefined options can be marked as resolved, because we only need // to resolve options with lazy closures, normalizers or validation @@ -403,6 +410,30 @@ public function getDefinedOptions(): array return array_keys($this->defined); } + /** + * Defines nested options. + * + * @param \Closure(self $resolver, Options $parent): void $nested + * + * @return $this + */ + public function setOptions(string $option, \Closure $nested): static + { + if ($this->locked) { + throw new AccessException('Nested options cannot be defined from a lazy option or normalizer.'); + } + + // Store closure for later evaluation + $this->nested[$option][] = $nested; + $this->defaults[$option] = []; + $this->defined[$option] = true; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + public function isNested(string $option): bool { return isset($this->nested[$option]); @@ -947,6 +978,23 @@ public function offsetGet(mixed $option, bool $triggerDeprecation = true): mixed $value = $this->defaults[$option]; + // Resolve the option if the default value is lazily evaluated + if (isset($this->lazy[$option])) { + // If the closure is already being called, we have a cyclic dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(\sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); + } + + $this->calling[$option] = true; + try { + foreach ($this->lazy[$option] as $closure) { + $value = $closure($this, $value); + } + } finally { + unset($this->calling[$option]); + } + } + // Resolve the option if it is a nested definition if (isset($this->nested[$option])) { // If the closure is already being called, we have a cyclic dependency @@ -958,7 +1006,6 @@ public function offsetGet(mixed $option, bool $triggerDeprecation = true): mixed throw new InvalidOptionsException(\sprintf('The nested option "%s" with value %s is expected to be of type array, but is of type "%s".', $this->formatOptions([$option]), $this->formatValue($value), get_debug_type($value))); } - // The following section must be protected from cyclic calls. $this->calling[$option] = true; try { $resolver = new self(); @@ -989,29 +1036,6 @@ public function offsetGet(mixed $option, bool $triggerDeprecation = true): mixed } } - // Resolve the option if the default value is lazily evaluated - if (isset($this->lazy[$option])) { - // If the closure is already being called, we have a cyclic - // dependency - if (isset($this->calling[$option])) { - throw new OptionDefinitionException(\sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); - } - - // The following section must be protected from cyclic - // calls. Set $calling for the current $option to detect a cyclic - // dependency - // BEGIN - $this->calling[$option] = true; - try { - foreach ($this->lazy[$option] as $closure) { - $value = $closure($this, $value); - } - } finally { - unset($this->calling[$option]); - } - // END - } - // Validate the type of the resolved option if (isset($this->allowedTypes[$option])) { $valid = true; diff --git a/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php b/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php index 404d29d0a38bc..c7f3f51be903a 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/Debug/OptionsResolverIntrospectorTest.php @@ -263,4 +263,13 @@ public function testGetDeprecationMessageThrowsOnNotDefinedOption() $debug = new OptionsResolverIntrospector($resolver); $debug->getDeprecation('foo'); } + + public function testGetClosureNested() + { + $resolver = new OptionsResolver(); + $resolver->setOptions('foo', $closure = function (OptionsResolver $resolver) {}); + + $debug = new OptionsResolverIntrospector($resolver); + $this->assertSame([$closure], $debug->getNestedOptions('foo')); + } } diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 96faa09b07d83..5684fb9c67d9e 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector; use Symfony\Component\OptionsResolver\Exception\AccessException; use Symfony\Component\OptionsResolver\Exception\InvalidArgumentException; @@ -26,6 +27,8 @@ class OptionsResolverTest extends TestCase { + use ExpectDeprecationTrait; + private OptionsResolver $resolver; protected function setUp(): void @@ -1091,29 +1094,41 @@ public function testFailIfSetAllowedValuesFromLazyOption() $this->resolver->resolve(); } - public function testResolveFailsIfInvalidValueFromNestedOption() + /** + * @group legacy + */ + public function testLegacyResolveFailsIfInvalidValueFromNestedOption() { - $this->expectException(InvalidOptionsException::class); - $this->expectExceptionMessage('The option "foo[bar]" with value "invalid value" is invalid. Accepted values are: "valid value".'); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefault('foo', function (OptionsResolver $resolver) { $resolver ->setDefined('bar') ->setAllowedValues('bar', 'valid value'); }); + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The option "foo[bar]" with value "invalid value" is invalid. Accepted values are: "valid value".'); + $this->resolver->resolve(['foo' => ['bar' => 'invalid value']]); } - public function testResolveFailsIfInvalidTypeFromNestedOption() + /** + * @group legacy + */ + public function testLegacyResolveFailsIfInvalidTypeFromNestedOption() { - $this->expectException(InvalidOptionsException::class); - $this->expectExceptionMessage('The option "foo[bar]" with value 1 is expected to be of type "string", but is of type "int".'); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefault('foo', function (OptionsResolver $resolver) { $resolver ->setDefined('bar') ->setAllowedTypes('bar', 'string'); }); + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The option "foo[bar]" with value 1 is expected to be of type "string", but is of type "int".'); + $this->resolver->resolve(['foo' => ['bar' => 1]]); } @@ -2096,8 +2111,13 @@ public function testNestedArrayException5() ]); } - public function testIsNestedOption() + /** + * @group legacy + */ + public function testLegacyIsNestedOption() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'database' => function (OptionsResolver $resolver) { $resolver->setDefined(['host', 'port']); @@ -2106,40 +2126,57 @@ public function testIsNestedOption() $this->assertTrue($this->resolver->isNested('database')); } - public function testFailsIfUndefinedNestedOption() + /** + * @group legacy + */ + public function testLegacyFailsIfUndefinedNestedOption() { - $this->expectException(UndefinedOptionsException::class); - $this->expectExceptionMessage('The option "database[foo]" does not exist. Defined options are: "host", "port".'); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'name' => 'default', 'database' => function (OptionsResolver $resolver) { $resolver->setDefined(['host', 'port']); }, ]); + + $this->expectException(UndefinedOptionsException::class); + $this->expectExceptionMessage('The option "database[foo]" does not exist. Defined options are: "host", "port".'); + $this->resolver->resolve([ 'database' => ['foo' => 'bar'], ]); } - public function testFailsIfMissingRequiredNestedOption() + /** + * @group legacy + */ + public function testLegacyFailsIfMissingRequiredNestedOption() { - $this->expectException(MissingOptionsException::class); - $this->expectExceptionMessage('The required option "database[host]" is missing.'); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'name' => 'default', 'database' => function (OptionsResolver $resolver) { $resolver->setRequired('host'); }, ]); + + $this->expectException(MissingOptionsException::class); + $this->expectExceptionMessage('The required option "database[host]" is missing.'); + $this->resolver->resolve([ 'database' => [], ]); } - public function testFailsIfInvalidTypeNestedOption() + /** + * @group legacy + */ + public function testLegacyFailsIfInvalidTypeNestedOption() { - $this->expectException(InvalidOptionsException::class); - $this->expectExceptionMessage('The option "database[logging]" with value null is expected to be of type "bool", but is of type "null".'); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'name' => 'default', 'database' => function (OptionsResolver $resolver) { @@ -2148,28 +2185,44 @@ public function testFailsIfInvalidTypeNestedOption() ->setAllowedTypes('logging', 'bool'); }, ]); + + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The option "database[logging]" with value null is expected to be of type "bool", but is of type "null".'); + $this->resolver->resolve([ 'database' => ['logging' => null], ]); } - public function testFailsIfNotArrayIsGivenForNestedOptions() + /** + * @group legacy + */ + public function testLegacyFailsIfNotArrayIsGivenForNestedOptions() { - $this->expectException(InvalidOptionsException::class); - $this->expectExceptionMessage('The nested option "database" with value null is expected to be of type array, but is of type "null".'); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'name' => 'default', 'database' => function (OptionsResolver $resolver) { $resolver->setDefined('host'); }, ]); + + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The nested option "database" with value null is expected to be of type array, but is of type "null".'); + $this->resolver->resolve([ 'database' => null, ]); } - public function testResolveNestedOptionsWithoutDefault() + /** + * @group legacy + */ + public function testLegacyResolveNestedOptionsWithoutDefault() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'name' => 'default', 'database' => function (OptionsResolver $resolver) { @@ -2184,8 +2237,13 @@ public function testResolveNestedOptionsWithoutDefault() $this->assertSame($expectedOptions, $actualOptions); } - public function testResolveNestedOptionsWithDefault() + /** + * @group legacy + */ + public function testLegacyResolveNestedOptionsWithDefault() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'name' => 'default', 'database' => function (OptionsResolver $resolver) { @@ -2206,8 +2264,13 @@ public function testResolveNestedOptionsWithDefault() $this->assertSame($expectedOptions, $actualOptions); } - public function testResolveMultipleNestedOptions() + /** + * @group legacy + */ + public function testLegacyResolveMultipleNestedOptions() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'name' => 'default', 'database' => function (OptionsResolver $resolver) { @@ -2245,8 +2308,13 @@ public function testResolveMultipleNestedOptions() $this->assertSame($expectedOptions, $actualOptions); } - public function testResolveLazyOptionUsingNestedOption() + /** + * @group legacy + */ + public function testLegacyResolveLazyOptionUsingNestedOption() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'version' => fn (Options $options) => $options['database']['server_version'], 'database' => function (OptionsResolver $resolver) { @@ -2261,8 +2329,13 @@ public function testResolveLazyOptionUsingNestedOption() $this->assertSame($expectedOptions, $actualOptions); } - public function testNormalizeNestedOptionValue() + /** + * @group legacy + */ + public function testLegacyNormalizeNestedOptionValue() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver ->setDefaults([ 'database' => function (OptionsResolver $resolver) { @@ -2287,8 +2360,13 @@ public function testNormalizeNestedOptionValue() $this->assertSame($expectedOptions, $actualOptions); } + /** + * @group legacy + */ public function testOverwrittenNestedOptionNotEvaluatedIfLazyDefault() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + // defined by superclass $this->resolver->setDefault('foo', function (OptionsResolver $resolver) { Assert::fail('Should not be called'); @@ -2298,8 +2376,13 @@ public function testOverwrittenNestedOptionNotEvaluatedIfLazyDefault() $this->assertSame(['foo' => 'lazy'], $this->resolver->resolve()); } + /** + * @group legacy + */ public function testOverwrittenNestedOptionNotEvaluatedIfScalarDefault() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + // defined by superclass $this->resolver->setDefault('foo', function (OptionsResolver $resolver) { Assert::fail('Should not be called'); @@ -2309,8 +2392,13 @@ public function testOverwrittenNestedOptionNotEvaluatedIfScalarDefault() $this->assertSame(['foo' => 'bar'], $this->resolver->resolve()); } + /** + * @group legacy + */ public function testOverwrittenLazyOptionNotEvaluatedIfNestedOption() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + // defined by superclass $this->resolver->setDefault('foo', function (Options $options) { Assert::fail('Should not be called'); @@ -2322,8 +2410,13 @@ public function testOverwrittenLazyOptionNotEvaluatedIfNestedOption() $this->assertSame(['foo' => ['bar' => 'baz']], $this->resolver->resolve()); } - public function testResolveAllNestedOptionDefinitions() + /** + * @group legacy + */ + public function testLegacyResolveAllNestedOptionDefinitions() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + // defined by superclass $this->resolver->setDefault('foo', function (OptionsResolver $resolver) { $resolver->setRequired('bar'); @@ -2339,8 +2432,13 @@ public function testResolveAllNestedOptionDefinitions() $this->assertSame(['foo' => ['ping' => 'pong', 'bar' => 'baz']], $this->resolver->resolve()); } - public function testNormalizeNestedValue() + /** + * @group legacy + */ + public function testLegacyNormalizeNestedValue() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + // defined by superclass $this->resolver->setDefault('foo', function (OptionsResolver $resolver) { $resolver->setDefault('bar', null); @@ -2354,30 +2452,48 @@ public function testNormalizeNestedValue() $this->assertSame(['foo' => ['bar' => 'baz']], $this->resolver->resolve()); } - public function testFailsIfCyclicDependencyBetweenSameNestedOption() + /** + * @group legacy + */ + public function testLegacyFailsIfCyclicDependencyBetweenSameNestedOption() { - $this->expectException(OptionDefinitionException::class); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefault('database', function (OptionsResolver $resolver, Options $parent) { $resolver->setDefault('replicas', $parent['database']); }); + + $this->expectException(OptionDefinitionException::class); + $this->resolver->resolve(); } - public function testFailsIfCyclicDependencyBetweenNestedOptionAndParentLazyOption() + /** + * @group legacy + */ + public function testLegacyFailsIfCyclicDependencyBetweenNestedOptionAndParentLazyOption() { - $this->expectException(OptionDefinitionException::class); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'version' => fn (Options $options) => $options['database']['server_version'], 'database' => function (OptionsResolver $resolver, Options $parent) { $resolver->setDefault('server_version', $parent['version']); }, ]); + + $this->expectException(OptionDefinitionException::class); + $this->resolver->resolve(); } - public function testFailsIfCyclicDependencyBetweenNormalizerAndNestedOption() + /** + * @group legacy + */ + public function testLegacyFailsIfCyclicDependencyBetweenNormalizerAndNestedOption() { - $this->expectException(OptionDefinitionException::class); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver ->setDefault('name', 'default') ->setDefault('database', function (OptionsResolver $resolver, Options $parent) { @@ -2386,23 +2502,38 @@ public function testFailsIfCyclicDependencyBetweenNormalizerAndNestedOption() ->setNormalizer('name', function (Options $options, $value) { $options['database']; }); + + $this->expectException(OptionDefinitionException::class); + $this->resolver->resolve(); } - public function testFailsIfCyclicDependencyBetweenNestedOptions() + /** + * @group legacy + */ + public function testLegacyFailsIfCyclicDependencyBetweenNestedOptions() { - $this->expectException(OptionDefinitionException::class); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefault('database', function (OptionsResolver $resolver, Options $parent) { $resolver->setDefault('host', $parent['replica']['host']); }); $this->resolver->setDefault('replica', function (OptionsResolver $resolver, Options $parent) { $resolver->setDefault('host', $parent['database']['host']); }); + + $this->expectException(OptionDefinitionException::class); + $this->resolver->resolve(); } - public function testGetAccessToParentOptionFromNestedOption() + /** + * @group legacy + */ + public function testLegacyGetAccessToParentOptionFromNestedOption() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'version' => 3.15, 'database' => function (OptionsResolver $resolver, Options $parent) { @@ -2430,8 +2561,13 @@ public function testNestedClosureWithoutTypeHint2ndArgumentNotInvoked() $this->assertSame(['foo' => $closure], $this->resolver->resolve()); } - public function testResolveLazyOptionWithTransitiveDefaultDependency() + /** + * @group legacy + */ + public function testLegacyResolveLazyOptionWithTransitiveDefaultDependency() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'ip' => null, 'database' => function (OptionsResolver $resolver, Options $parent) { @@ -2454,8 +2590,13 @@ public function testResolveLazyOptionWithTransitiveDefaultDependency() $this->assertSame($expectedOptions, $actualOptions); } - public function testAccessToParentOptionFromNestedNormalizerAndLazyOption() + /** + * @group legacy + */ + public function testLegacyAccessToParentOptionFromNestedNormalizerAndLazyOption() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver->setDefaults([ 'debug' => true, 'database' => function (OptionsResolver $resolver, Options $parent) { @@ -2495,6 +2636,11 @@ public function testResolveOptionsDefinedByOptionConfigurator() ->normalize(static fn (Options $options, $value) => $value) ->info('info message') ; + $this->resolver->define('table') + ->options(function (OptionsResolver $resolver) { + $resolver->setDefault('ping', 'pong'); + }) + ; $introspector = new OptionsResolverIntrospector($this->resolver); $this->assertTrue(true, $this->resolver->isDefined('foo')); @@ -2505,6 +2651,7 @@ public function testResolveOptionsDefinedByOptionConfigurator() $this->assertSame(['bar', 'zab'], $introspector->getAllowedValues('foo')); $this->assertCount(1, $introspector->getNormalizers('foo')); $this->assertSame('info message', $this->resolver->getInfo('foo')); + $this->assertTrue($this->resolver->isNested('table')); } public function testGetInfo() @@ -2529,6 +2676,18 @@ public function testSetInfoOnNormalization() $this->resolver->resolve(['foo' => 'bar']); } + public function testSetNestedOnNormalization() + { + $this->expectException(AccessException::class); + $this->expectExceptionMessage('Nested options cannot be defined from a lazy option or normalizer.'); + + $this->resolver->setDefault('foo', function (Options $options) { + $options->setOptions('foo', function () {}); + }); + + $this->resolver->resolve(); + } + public function testSetInfoOnUndefinedOption() { $this->expectException(UndefinedOptionsException::class); @@ -2562,36 +2721,42 @@ public function testInfoOnInvalidValue() $this->resolver->resolve(['expires' => new \DateTimeImmutable('-1 hour')]); } - public function testInvalidValueForPrototypeDefinition() + /** + * @group legacy + */ + public function testLegacyInvalidValueForPrototypeDefinition() { - $this->expectException(InvalidOptionsException::class); - $this->expectExceptionMessage('The value of the option "connections" is expected to be of type array of array, but is of type array of "string".'); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); $this->resolver ->setDefault('connections', static function (OptionsResolver $resolver) { $resolver ->setPrototype(true) - ->setDefined(['table', 'user', 'password']) - ; - }) - ; + ->setDefined(['table', 'user', 'password']); + }); + + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The value of the option "connections" is expected to be of type array of array, but is of type array of "string".'); $this->resolver->resolve(['connections' => ['foo']]); } - public function testMissingOptionForPrototypeDefinition() + /** + * @group legacy + */ + public function testLegacyMissingOptionForPrototypeDefinition() { - $this->expectException(MissingOptionsException::class); - $this->expectExceptionMessage('The required option "connections[1][table]" is missing.'); + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); $this->resolver ->setDefault('connections', static function (OptionsResolver $resolver) { $resolver ->setPrototype(true) - ->setRequired('table') - ; - }) - ; + ->setRequired('table'); + }); + + $this->expectException(MissingOptionsException::class); + $this->expectExceptionMessage('The required option "connections[1][table]" is missing.'); $this->resolver->resolve(['connections' => [ ['table' => 'default'], @@ -2607,8 +2772,13 @@ public function testAccessExceptionOnPrototypeDefinition() $this->resolver->setPrototype(true); } - public function testPrototypeDefinition() + /** + * @group legacy + */ + public function testLegacyPrototypeDefinition() { + $this->expectDeprecation('Since symfony/options-resolver 7.3: Defining nested options via "Symfony\Component\OptionsResolver\OptionsResolver::setDefault()" is deprecated and will be removed in Symfony 8.0, use "setOptions()" method instead.'); + $this->resolver ->setDefault('connections', static function (OptionsResolver $resolver) { $resolver @@ -2648,4 +2818,510 @@ public function testPrototypeDefinition() $this->assertSame($expectedOptions, $actualOptions); } + + public function testPrototypeDefinition() + { + $this->resolver + ->setOptions('connections', static function (OptionsResolver $resolver) { + $resolver + ->setPrototype(true) + ->setRequired('table') + ->setDefaults(['user' => 'root', 'password' => null]); + }); + + $actualOptions = $this->resolver->resolve([ + 'connections' => [ + 'default' => [ + 'table' => 'default', + ], + 'custom' => [ + 'user' => 'foo', + 'password' => 'pa$$', + 'table' => 'symfony', + ], + ], + ]); + $expectedOptions = [ + 'connections' => [ + 'default' => [ + 'user' => 'root', + 'password' => null, + 'table' => 'default', + ], + 'custom' => [ + 'user' => 'foo', + 'password' => 'pa$$', + 'table' => 'symfony', + ], + ], + ]; + + $this->assertSame($expectedOptions, $actualOptions); + } + + public function testInvalidValueForPrototypeDefinition() + { + $this->resolver + ->setOptions('connections', static function (OptionsResolver $resolver) { + $resolver + ->setPrototype(true) + ->setDefined(['table', 'user', 'password']); + }); + + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The value of the option "connections" is expected to be of type array of array, but is of type array of "string".'); + + $this->resolver->resolve(['connections' => ['foo']]); + } + + public function testMissingOptionForPrototypeDefinition() + { + $this->resolver + ->setOptions('connections', static function (OptionsResolver $resolver) { + $resolver + ->setPrototype(true) + ->setRequired('table'); + }); + + $this->expectException(MissingOptionsException::class); + $this->expectExceptionMessage('The required option "connections[1][table]" is missing.'); + + $this->resolver->resolve(['connections' => [ + ['table' => 'default'], + [], // <- missing required option "table" + ]]); + } + + public function testResolveFailsIfInvalidValueFromNestedOption() + { + $this->resolver->setOptions('foo', function (OptionsResolver $resolver) { + $resolver + ->setDefined('bar') + ->setAllowedValues('bar', 'valid value'); + }); + + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The option "foo[bar]" with value "invalid value" is invalid. Accepted values are: "valid value".'); + + $this->resolver->resolve(['foo' => ['bar' => 'invalid value']]); + } + + public function testResolveFailsIfInvalidTypeFromNestedOption() + { + $this->resolver->setOptions('foo', function (OptionsResolver $resolver) { + $resolver + ->setDefined('bar') + ->setAllowedTypes('bar', 'string'); + }); + + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The option "foo[bar]" with value 1 is expected to be of type "string", but is of type "int".'); + + $this->resolver->resolve(['foo' => ['bar' => 1]]); + } + + public function testIsNestedOption() + { + $this->resolver->setOptions('database', function (OptionsResolver $resolver) { + $resolver->setDefined(['host', 'port']); + }); + + $this->assertTrue($this->resolver->isNested('database')); + } + + public function testFailsIfUndefinedNestedOption() + { + $this->resolver + ->setDefault('name', 'default') + ->setOptions('database', function (OptionsResolver $resolver) { + $resolver->setDefined(['host', 'port']); + }); + + $this->expectException(UndefinedOptionsException::class); + $this->expectExceptionMessage('The option "database[foo]" does not exist. Defined options are: "host", "port".'); + + $this->resolver->resolve([ + 'database' => ['foo' => 'bar'], + ]); + } + + public function testFailsIfMissingRequiredNestedOption() + { + $this->resolver + ->setDefault('name', 'default') + ->setOptions('database', function (OptionsResolver $resolver) { + $resolver->setRequired('host'); + }); + + $this->expectException(MissingOptionsException::class); + $this->expectExceptionMessage('The required option "database[host]" is missing.'); + + $this->resolver->resolve([ + 'database' => [], + ]); + } + + public function testFailsIfInvalidTypeNestedOption() + { + $this->resolver + ->setDefault('name', 'default') + ->setOptions('database', function (OptionsResolver $resolver) { + $resolver + ->setDefined('logging') + ->setAllowedTypes('logging', 'bool'); + }); + + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The option "database[logging]" with value null is expected to be of type "bool", but is of type "null".'); + + $this->resolver->resolve([ + 'database' => ['logging' => null], + ]); + } + + public function testFailsIfNotArrayIsGivenForNestedOptions() + { + $this->resolver + ->setDefault('name', 'default') + ->setOptions('database', function (OptionsResolver $resolver) { + $resolver->setDefined('host'); + }); + + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The nested option "database" with value null is expected to be of type array, but is of type "null".'); + + $this->resolver->resolve([ + 'database' => null, + ]); + } + + public function testResolveNestedOptionsWithoutDefault() + { + $this->resolver + ->setDefault('name', 'default') + ->setOptions('database', function (OptionsResolver $resolver) { + $resolver->setDefined(['host', 'port']); + }); + + $actualOptions = $this->resolver->resolve(); + $expectedOptions = [ + 'name' => 'default', + 'database' => [], + ]; + + $this->assertSame($expectedOptions, $actualOptions); + } + + public function testResolveNestedOptionsWithDefault() + { + $this->resolver + ->setDefault('name', 'default') + ->setOptions('database', function (OptionsResolver $resolver) { + $resolver->setDefaults([ + 'host' => 'localhost', + 'port' => 3306, + ]); + }); + + $actualOptions = $this->resolver->resolve(); + $expectedOptions = [ + 'name' => 'default', + 'database' => [ + 'host' => 'localhost', + 'port' => 3306, + ], + ]; + + $this->assertSame($expectedOptions, $actualOptions); + } + + public function testResolveMultipleNestedOptions() + { + $this->resolver + ->setDefaults(['name' => 'default']) + ->setOptions('database', function (OptionsResolver $resolver) { + $resolver + ->setRequired(['dbname', 'host']) + ->setDefaults(['port' => 3306]) + ->setOptions('replicas', function (OptionsResolver $resolver) { + $resolver->setDefaults([ + 'host' => 'replica1', + 'port' => 3306, + ]); + }); + }); + + $actualOptions = $this->resolver->resolve([ + 'name' => 'custom', + 'database' => [ + 'dbname' => 'test', + 'host' => 'localhost', + 'port' => null, + 'replicas' => ['host' => 'replica2'], + ], + ]); + $expectedOptions = [ + 'name' => 'custom', + 'database' => [ + 'port' => null, + 'replicas' => ['port' => 3306, 'host' => 'replica2'], + 'dbname' => 'test', + 'host' => 'localhost', + ], + ]; + $this->assertSame($expectedOptions, $actualOptions); + } + + public function testResolveLazyOptionUsingNestedOption() + { + $this->resolver + ->setDefault('version', function (Options $options) { + return $options['database']['server_version']; + }) + ->setOptions('database', function (OptionsResolver $resolver) { + $resolver->setDefault('server_version', '3.15'); + }); + + $actualOptions = $this->resolver->resolve(); + $expectedOptions = [ + 'database' => ['server_version' => '3.15'], + 'version' => '3.15', + ]; + + $this->assertSame($expectedOptions, $actualOptions); + } + + public function testNormalizeNestedOptionValue() + { + $this->resolver + ->setOptions('database', function (OptionsResolver $resolver) { + $resolver->setDefaults([ + 'port' => 3306, + 'host' => 'localhost', + 'dbname' => 'demo', + ]); + }) + ->setNormalizer('database', function (Options $options, $value) { + ksort($value); + + return $value; + }); + + $actualOptions = $this->resolver->resolve([ + 'database' => ['dbname' => 'test'], + ]); + $expectedOptions = [ + 'database' => ['dbname' => 'test', 'host' => 'localhost', 'port' => 3306], + ]; + + $this->assertSame($expectedOptions, $actualOptions); + } + + public function testNestedOptionEvaluatedWithLazyDefault() + { + // defined by superclass + $this->resolver->setOptions('foo', function (OptionsResolver $resolver) { + $resolver->define('bar')->allowedTypes('string'); + }); + // defined by subclass + $this->resolver->setDefault('foo', fn (Options $options) => ['bar' => 'lazy']); + + $this->assertSame(['foo' => ['bar' => 'lazy']], $this->resolver->resolve()); + } + + public function testNestedOptionWithDefault() + { + // defined by superclass + $this->resolver->setOptions('foo', function (OptionsResolver $resolver) { + $resolver->define('bar')->allowedTypes('string'); + }); + // defined by subclass + $this->resolver->setDefault('foo', ['bar' => 'default']); + + $this->assertSame(['foo' => ['bar' => 'default']], $this->resolver->resolve()); + } + + public function testResolveAllNestedOptionDefinitions() + { + // defined by superclass + $this->resolver->setOptions('foo', function (OptionsResolver $resolver) { + $resolver->setRequired('bar'); + }); + // defined by subclass + $this->resolver->setOptions('foo', function (OptionsResolver $resolver) { + $resolver->setDefault('bar', 'baz'); + }); + // defined by subclass + $this->resolver->setOptions('foo', function (OptionsResolver $resolver) { + $resolver->setDefault('ping', 'pong'); + }); + $this->assertSame(['foo' => ['ping' => 'pong', 'bar' => 'baz']], $this->resolver->resolve()); + } + + public function testSetNestedOptionWithInvalidDefault() + { + // defined by superclass + $this->resolver->setOptions('foo', function (OptionsResolver $resolver) { + $resolver->define('bar')->allowedTypes('int'); + }); + // defined by subclass + $this->resolver->setDefault('foo', ['bar' => 'invalid']); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The option "foo[bar]" with value "invalid" is expected to be of type "int", but is of type "string".'); + + $this->resolver->resolve(); + } + + public function testSetNestedOptionWithInvalidLazyDefault() + { + // defined by superclass + $this->resolver->setOptions('foo', function (OptionsResolver $resolver) { + $resolver->define('bar')->allowedTypes('int'); + }); + // defined by subclass + $this->resolver->setDefault('foo', function (Options $options) { + return ['bar' => 'invalid']; + }); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The option "foo[bar]" with value "invalid" is expected to be of type "int", but is of type "string".'); + + $this->resolver->resolve(); + } + + public function testNormalizeNestedValue() + { + // defined by superclass + $this->resolver->setOptions('foo', function (OptionsResolver $resolver) { + $resolver->setDefault('bar', null); + }); + // defined by subclass + $this->resolver->setNormalizer('foo', function (Options $options, $resolvedValue) { + $resolvedValue['bar'] ??= 'baz'; + + return $resolvedValue; + }); + + $this->assertSame(['foo' => ['bar' => 'baz']], $this->resolver->resolve()); + } + + public function testFailsIfCyclicDependencyBetweenSameNestedOption() + { + $this->resolver->setOptions('database', function (OptionsResolver $resolver, Options $parent) { + $resolver->setDefault('replicas', $parent['database']); + }); + + $this->expectException(OptionDefinitionException::class); + + $this->resolver->resolve(); + } + + public function testFailsIfCyclicDependencyBetweenNestedOptionAndParentLazyOption() + { + $this->resolver + ->setDefault('version', function (Options $options) { + return $options['database']['server_version']; + }) + ->setOptions('database', function (OptionsResolver $resolver, Options $parent) { + $resolver->setDefault('server_version', $parent['version']); + }); + + $this->expectException(OptionDefinitionException::class); + + $this->resolver->resolve(); + } + + public function testFailsIfCyclicDependencyBetweenNormalizerAndNestedOption() + { + $this->resolver + ->setDefault('name', 'default') + ->setOptions('database', function (OptionsResolver $resolver, Options $parent) { + $resolver->setDefault('host', $parent['name']); + }) + ->setNormalizer('name', function (Options $options, $value) { + $options['database']; + }); + + $this->expectException(OptionDefinitionException::class); + + $this->resolver->resolve(); + } + + public function testFailsIfCyclicDependencyBetweenNestedOptions() + { + $this->resolver->setOptions('database', function (OptionsResolver $resolver, Options $parent) { + $resolver->setDefault('host', $parent['replica']['host']); + }); + $this->resolver->setOptions('replica', function (OptionsResolver $resolver, Options $parent) { + $resolver->setDefault('host', $parent['database']['host']); + }); + + $this->expectException(OptionDefinitionException::class); + + $this->resolver->resolve(); + } + + public function testGetAccessToParentOptionFromNestedOption() + { + $this->resolver + ->setDefault('version', 3.15) + ->setOptions('database', function (OptionsResolver $resolver, Options $parent) { + $resolver->setDefault('server_version', $parent['version']); + }); + + $this->assertSame(['version' => 3.15, 'database' => ['server_version' => 3.15]], $this->resolver->resolve()); + } + + public function testResolveLazyOptionWithTransitiveDefaultDependency() + { + $this->resolver + ->setDefaults([ + 'ip' => null, + 'secondary_replica' => function (Options $options) { + return $options['database']['primary_replica']['host']; + }, + ]) + ->setOptions('database', function (OptionsResolver $resolver, Options $parent) { + $resolver + ->setDefault('host', $parent['ip']) + ->setOptions('primary_replica', function (OptionsResolver $resolver, Options $parent) { + $resolver->setDefault('host', $parent['host']); + }); + }); + + $actualOptions = $this->resolver->resolve(['ip' => '127.0.0.1']); + $expectedOptions = [ + 'ip' => '127.0.0.1', + 'database' => [ + 'host' => '127.0.0.1', + 'primary_replica' => ['host' => '127.0.0.1'], + ], + 'secondary_replica' => '127.0.0.1', + ]; + $this->assertSame($expectedOptions, $actualOptions); + } + + public function testAccessToParentOptionFromNestedNormalizerAndLazyOption() + { + $this->resolver + ->setDefault('debug', true) + ->setOptions('database', function (OptionsResolver $resolver, Options $parent) { + $resolver + ->setDefined('logging') + ->setDefault('profiling', fn (Options $options) => $parent['debug']) + ->setNormalizer('logging', fn (Options $options, $value) => false === $parent['debug'] ? true : $value); + }); + + $actualOptions = $this->resolver->resolve([ + 'debug' => false, + 'database' => ['logging' => false], + ]); + $expectedOptions = [ + 'debug' => false, + 'database' => ['profiling' => false, 'logging' => true], + ]; + + $this->assertSame($expectedOptions, $actualOptions); + } } diff --git a/src/Symfony/Component/RateLimiter/RateLimiterFactory.php b/src/Symfony/Component/RateLimiter/RateLimiterFactory.php index 2c27ede775541..f9d0ca9a7386e 100644 --- a/src/Symfony/Component/RateLimiter/RateLimiterFactory.php +++ b/src/Symfony/Component/RateLimiter/RateLimiterFactory.php @@ -82,7 +82,7 @@ private static function configureOptions(OptionsResolver $options): void ->define('limit')->allowedTypes('int') ->define('interval')->allowedTypes('string')->normalize($intervalNormalizer) ->define('rate') - ->default(function (OptionsResolver $rate) use ($intervalNormalizer) { + ->options(function (OptionsResolver $rate) use ($intervalNormalizer) { $rate ->define('amount')->allowedTypes('int')->default(1) ->define('interval')->allowedTypes('string')->normalize($intervalNormalizer) diff --git a/src/Symfony/Component/RateLimiter/composer.json b/src/Symfony/Component/RateLimiter/composer.json index 9f5bfcdb33e6d..fdf0e01c4979b 100644 --- a/src/Symfony/Component/RateLimiter/composer.json +++ b/src/Symfony/Component/RateLimiter/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": ">=8.2", - "symfony/options-resolver": "^6.4|^7.0" + "symfony/options-resolver": "^7.3" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.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