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 #27291 [OptionsResolver] Added support for nesting options definition (yceruto)
This PR was merged into the 4.2-dev branch.
Discussion
----------
[OptionsResolver] Added support for nesting options definition
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | #4833
| License | MIT
| Doc PR | symfony/symfony-docs#9995
I'd like to propose an alternative to #27251 and #18134 with a different approach.
It would allow you to create a nested options system with required options, validation (type, value),
normalization and more.
<details>
<summary><strong>Short documentation</strong></summary>
**To define a nested option, you can pass a closure as the default value of the option with an `OptionsResolver` argument:**
```php
$resolver
->defaults([
'connection' => 'default',
'database' => function (OptionsResolver $resolver) {
$resolver
->setRequired(['dbname', 'host', ...])
->setDefaults([
'driver' => 'pdo_sqlite',
'port' => function (Options $options) {
return 'pdo_mysql' === $options['driver'] ? 3306 : null,
},
'logging' => true,
])
->setAllowedValues('driver', ['pdo_sqlite', 'pdo_mysql'])
->setAllowedTypes('port', 'int')
->setAllowedTypes('logging', 'bool')
// ...
},
]);
$resolver->resolve(array(
'database' => array(
'dbname' => 'demo',
'host' => 'localhost',
'driver' => 'pdo_mysql',
),
));
// returns: array(
// 'connection' => 'default',
// 'database' => array(
// 'dbname' => 'demo',
// 'host' => 'localhost',
// 'driver' => 'pdo_mysql',
// 'port' => 3306,
// 'logging' => true,
// ),
//)
```
Based on this instance, you can define the options under ``database`` and its desired default
value.
**If the default value of a child option depend on another option defined in parent level,
adds a second ``Options`` argument to the closure:**
```php
$resolver
->defaults([
'profiling' => false,
'database' => function (OptionsResolver $resolver, Options $parent) {
$resolver
->setDefault('logging', $parent['profiling'])
->setAllowedTypes('logging', 'bool');
},
])
;
```
**Access to nested options from lazy or normalize functions in parent level:**
```php
$resolver
->defaults([
'version' => function (Options $options) {
return $options['database']['server_version'];
},
'database' => function (OptionsResolver $resolver) {
$resolver
->setDefault('server_version', 3.15)
->setAllowedTypes('server_version', 'numeric')
// ...
},
])
;
```
As condition, for nested options you must to pass an array of values to resolve it on runtime, otherwise an exception will be thrown:
```php
$resolver->resolve(); // OK
$resolver->resolve(['database' => []]); // OK
$resolver->resolve(['database' => null); // KO (Exception!)
```
</details>
---
Demo app https://github.com/yceruto/nested-optionsresolver-demo
Commits
-------
d04e40b Added support for nested options definition
// Yet undefined options can be marked as resolved, because we only need
181
214
// to resolve options with lazy closures, normalizers or validation
@@ -354,6 +387,11 @@ public function getDefinedOptions()
354
387
returnarray_keys($this->defined);
355
388
}
356
389
390
+
publicfunctionisNested(string$option): bool
391
+
{
392
+
returnisset($this->nested[$option]);
393
+
}
394
+
357
395
/**
358
396
* Deprecates an option, allowed types or values.
359
397
*
@@ -649,6 +687,7 @@ public function clear()
649
687
650
688
$this->defined = array();
651
689
$this->defaults = array();
690
+
$this->nested = array();
652
691
$this->required = array();
653
692
$this->resolved = array();
654
693
$this->lazy = array();
@@ -767,6 +806,32 @@ public function offsetGet($option)
767
806
768
807
$value = $this->defaults[$option];
769
808
809
+
// Resolve the option if it is a nested definition
810
+
if (isset($this->nested[$option])) {
811
+
// If the closure is already being called, we have a cyclic dependency
812
+
if (isset($this->calling[$option])) {
813
+
thrownewOptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
814
+
}
815
+
816
+
if (!\is_array($value)) {
817
+
thrownewInvalidOptionsException(sprintf('The nested option "%s" with value %s is expected to be of type array, but is of type "%s".', $option, $this->formatValue($value), $this->formatTypeOf($value, 'array')));
818
+
}
819
+
820
+
// The following section must be protected from cyclic calls.
821
+
// BEGIN
822
+
$this->calling[$option] = true;
823
+
try {
824
+
$resolver = newself();
825
+
foreach ($this->nested[$option] as$closure) {
826
+
$closure($resolver, $this);
827
+
}
828
+
$value = $resolver->resolve($value);
829
+
} finally {
830
+
unset($this->calling[$option]);
831
+
}
832
+
// END
833
+
}
834
+
770
835
// Resolve the option if the default value is lazily evaluated
771
836
if (isset($this->lazy[$option])) {
772
837
// If the closure is already being called, we have a cyclic
0 commit comments