Skip to content

[OptionsResolver] Deprecate defining nested options via setDefault() use setOptions() instead #59618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 26, 2025
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
19 changes: 19 additions & 0 deletions UPGRADE-7.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------

Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']) {
Expand Down
3 changes: 1 addition & 2 deletions src/Symfony/Component/Ldap/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/OptionsResolver/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
14 changes: 14 additions & 0 deletions src/Symfony/Component/OptionsResolver/OptionConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
108 changes: 66 additions & 42 deletions src/Symfony/Component/OptionsResolver/OptionsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ class OptionsResolver implements Options
*/
private array $nested = [];

/**
* BC layer. Remove in Symfony 8.0.
*
* @var array<string, true>
*/
private array $deprecatedNestedOptions = [];

/**
* The names of required options.
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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] = [];
Expand All @@ -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
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
}
Loading
Loading