You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feature #59618 [OptionsResolver] Deprecate defining nested options via setDefault() use setOptions() instead (yceruto)
This PR was merged into the 7.3 branch.
Discussion
----------
[OptionsResolver] Deprecate defining nested options via `setDefault()` use `setOptions()` instead
| Q | A
| ------------- | ---
| Branch? | 7.3
| Bug fix? | no
| New feature? | yes
| Deprecations? | yes
| Issues | -
| License | MIT
this removes unnecessary limitations that I hadn't considered when introducing nested options feature in #27291.
#### 1. Allow defining default values for nested options
Imagine you want to define the following nested option in a generic form type:
```php
class GenericType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('foo', function (OptionsResolver $foo) {
$foo->define('bar')->allowedTypes('int');
});
}
}
```
then, I'd like to define a concrete type with a default value for it.
```php
class ConcreteType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('foo', ['bar' => 23]);
}
public function getParent(): string
{
return GenericType::class;
}
}
```
this might seem unexpected, but the fact is that the nested definition for `foo` option in `ConcreteType` is gone. As a result, when resolved, the `foo` option will have a default value (`['bar' => 23]`) but without any constraints, allowing end users any value/type to be passed through this option for `ConcreteType` instances
For example, passing `['foo' => false]` as options for `ConcreteType` would be allowed, instead of requiring an array where `bar` expects an integer value.
#### 2. Allow defining lazy default for nested options
the same problem would occur with a lazy default for a nested definition:
```php
class ConcreteType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(['baz'])
$resolver->setDefault('foo', function (Options $options) {
return ['bar' => $options['baz']];
});
}
public function getParent(): string
{
return GenericType::class;
}
}
```
the issue here is the same as in the previous example, meaning this new default essentially overrides/removes the original nested definition
---
the two features mentioned earlier are now supported by introducing a new method `setOptions()`, which separates the nested definition from its default value (whether direct or lazy). Additionally this PR deprecates the practice of defining nested options using `setDefault()` method
this also enables the ability to set default values for prototyped options, which wasn't possible before. For example:
```php
class NavigatorType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->define('buttons')
->options(function (OptionsResolver $buttons) {
$buttons->setPrototype(true);
$buttons->define('name')->required()->allowedTypes('string');
$buttons->define('type')->default(SubmitType::class)->allowedTypes('string');
$buttons->define('options')->default([])->allowedTypes('array');
})
->default([
'back' => ['name' => 'back', 'options' => ['validate' => false, 'validation_groups' => false]],
'next' => ['name' => 'next'],
'submit' => ['name' => 'submit'],
]);
}
}
```
cheers!
Commits
-------
b0bb9a1 Add setOptions method
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__);
236
+
$this->deprecatedNestedOptions[$option] = true;
237
+
236
238
// Store closure for later evaluation
237
239
$this->nested[$option][] = $value;
238
240
$this->defaults[$option] = [];
@@ -245,8 +247,13 @@ public function setDefault(string $option, mixed $value): static
thrownewAccessException('Nested options cannot be defined from a lazy option or normalizer.');
424
+
}
425
+
426
+
// Store closure for later evaluation
427
+
$this->nested[$option][] = $nested;
428
+
$this->defaults[$option] = [];
429
+
$this->defined[$option] = true;
430
+
431
+
// Make sure the option is processed
432
+
unset($this->resolved[$option]);
433
+
434
+
return$this;
435
+
}
436
+
406
437
publicfunctionisNested(string$option): bool
407
438
{
408
439
returnisset($this->nested[$option]);
@@ -947,6 +978,23 @@ public function offsetGet(mixed $option, bool $triggerDeprecation = true): mixed
947
978
948
979
$value = $this->defaults[$option];
949
980
981
+
// Resolve the option if the default value is lazily evaluated
982
+
if (isset($this->lazy[$option])) {
983
+
// If the closure is already being called, we have a cyclic dependency
984
+
if (isset($this->calling[$option])) {
985
+
thrownewOptionDefinitionException(\sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling))));
986
+
}
987
+
988
+
$this->calling[$option] = true;
989
+
try {
990
+
foreach ($this->lazy[$option] as$closure) {
991
+
$value = $closure($this, $value);
992
+
}
993
+
} finally {
994
+
unset($this->calling[$option]);
995
+
}
996
+
}
997
+
950
998
// Resolve the option if it is a nested definition
951
999
if (isset($this->nested[$option])) {
952
1000
// 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
958
1006
thrownewInvalidOptionsException(\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)));
959
1007
}
960
1008
961
-
// The following section must be protected from cyclic calls.
962
1009
$this->calling[$option] = true;
963
1010
try {
964
1011
$resolver = newself();
@@ -989,29 +1036,6 @@ public function offsetGet(mixed $option, bool $triggerDeprecation = true): mixed
989
1036
}
990
1037
}
991
1038
992
-
// Resolve the option if the default value is lazily evaluated
993
-
if (isset($this->lazy[$option])) {
994
-
// If the closure is already being called, we have a cyclic
995
-
// dependency
996
-
if (isset($this->calling[$option])) {
997
-
thrownewOptionDefinitionException(\sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling))));
998
-
}
999
-
1000
-
// The following section must be protected from cyclic
1001
-
// calls. Set $calling for the current $option to detect a cyclic
0 commit comments