feature #33295 [OptionsResolver] Display full nested option hierarchy in exceptions (fancyweb)
This PR was merged into the 4.4 branch.
Discussion
----------
[OptionsResolver] Display full nested option hierarchy in exceptions
| Q | A
| ------------- | ---
| Branch? | 4.4
| Bug fix? | no
| New feature? | no
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | -
| License | MIT
| Doc PR | -
It kind of improve the DX, especially when you define a lot of nested form options since the file and line cannot be displayed.
```php
$resolver->setDefaults([
'array' => function (OptionsResolver $arrayResolver): void {
$arrayResolver->setRequired('foo');
},
]);
```
Before:
`The required option "foo" is missing.`
After:
`The required option "array[foo]" is missing.`
That can go to 4.3 I guess.
Commits
-------
a981fc3b50
[OptionsResolver] Display full nested options hierarchy in exceptions
This commit is contained in:
commit
e776419cee
@ -103,6 +103,8 @@ class OptionsResolver implements Options
|
||||
*/
|
||||
private $locked = false;
|
||||
|
||||
private $parentsOptions = [];
|
||||
|
||||
private static $typeAliases = [
|
||||
'boolean' => 'bool',
|
||||
'integer' => 'int',
|
||||
@ -423,7 +425,7 @@ class OptionsResolver implements Options
|
||||
}
|
||||
|
||||
if (!isset($this->defined[$option])) {
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist, defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist, defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined))));
|
||||
}
|
||||
|
||||
if (!\is_string($deprecationMessage) && !$deprecationMessage instanceof \Closure) {
|
||||
@ -481,7 +483,7 @@ class OptionsResolver implements Options
|
||||
}
|
||||
|
||||
if (!isset($this->defined[$option])) {
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined))));
|
||||
}
|
||||
|
||||
$this->normalizers[$option] = [$normalizer];
|
||||
@ -526,7 +528,7 @@ class OptionsResolver implements Options
|
||||
}
|
||||
|
||||
if (!isset($this->defined[$option])) {
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined))));
|
||||
}
|
||||
|
||||
if ($forcePrepend) {
|
||||
@ -569,7 +571,7 @@ class OptionsResolver implements Options
|
||||
}
|
||||
|
||||
if (!isset($this->defined[$option])) {
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined))));
|
||||
}
|
||||
|
||||
$this->allowedValues[$option] = \is_array($allowedValues) ? $allowedValues : [$allowedValues];
|
||||
@ -610,7 +612,7 @@ class OptionsResolver implements Options
|
||||
}
|
||||
|
||||
if (!isset($this->defined[$option])) {
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined))));
|
||||
}
|
||||
|
||||
if (!\is_array($allowedValues)) {
|
||||
@ -651,7 +653,7 @@ class OptionsResolver implements Options
|
||||
}
|
||||
|
||||
if (!isset($this->defined[$option])) {
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined))));
|
||||
}
|
||||
|
||||
$this->allowedTypes[$option] = (array) $allowedTypes;
|
||||
@ -686,7 +688,7 @@ class OptionsResolver implements Options
|
||||
}
|
||||
|
||||
if (!isset($this->defined[$option])) {
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
|
||||
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined))));
|
||||
}
|
||||
|
||||
if (!isset($this->allowedTypes[$option])) {
|
||||
@ -793,7 +795,7 @@ class OptionsResolver implements Options
|
||||
ksort($clone->defined);
|
||||
ksort($diff);
|
||||
|
||||
throw new UndefinedOptionsException(sprintf((\count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Defined options are: "%s".', implode('", "', array_keys($diff)), implode('", "', array_keys($clone->defined))));
|
||||
throw new UndefinedOptionsException(sprintf((\count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Defined options are: "%s".', $this->formatOptions(array_keys($diff)), implode('", "', array_keys($clone->defined))));
|
||||
}
|
||||
|
||||
// Override options set by the user
|
||||
@ -809,7 +811,7 @@ class OptionsResolver implements Options
|
||||
if (\count($diff) > 0) {
|
||||
ksort($diff);
|
||||
|
||||
throw new MissingOptionsException(sprintf(\count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', implode('", "', array_keys($diff))));
|
||||
throw new MissingOptionsException(sprintf(\count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', $this->formatOptions(array_keys($diff))));
|
||||
}
|
||||
|
||||
// Lock the container
|
||||
@ -860,10 +862,10 @@ class OptionsResolver implements Options
|
||||
// Check whether the option is set at all
|
||||
if (!isset($this->defaults[$option]) && !\array_key_exists($option, $this->defaults)) {
|
||||
if (!isset($this->defined[$option])) {
|
||||
throw new NoSuchOptionException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
|
||||
throw new NoSuchOptionException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined))));
|
||||
}
|
||||
|
||||
throw new NoSuchOptionException(sprintf('The optional option "%s" has no value set. You should make sure it is set with "isset" before reading it.', $option));
|
||||
throw new NoSuchOptionException(sprintf('The optional option "%s" has no value set. You should make sure it is set with "isset" before reading it.', $this->formatOptions([$option])));
|
||||
}
|
||||
|
||||
$value = $this->defaults[$option];
|
||||
@ -872,17 +874,19 @@ class OptionsResolver implements Options
|
||||
if (isset($this->nested[$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.', implode('", "', array_keys($this->calling))));
|
||||
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling))));
|
||||
}
|
||||
|
||||
if (!\is_array($value)) {
|
||||
throw new InvalidOptionsException(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)));
|
||||
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), $this->formatTypeOf($value)));
|
||||
}
|
||||
|
||||
// The following section must be protected from cyclic calls.
|
||||
$this->calling[$option] = true;
|
||||
try {
|
||||
$resolver = new self();
|
||||
$resolver->parentsOptions = $this->parentsOptions;
|
||||
$resolver->parentsOptions[] = $option;
|
||||
foreach ($this->nested[$option] as $closure) {
|
||||
$closure($resolver, $this);
|
||||
}
|
||||
@ -897,7 +901,7 @@ class OptionsResolver implements Options
|
||||
// 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.', implode('", "', array_keys($this->calling))));
|
||||
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
|
||||
@ -932,10 +936,10 @@ class OptionsResolver implements Options
|
||||
$keys = array_keys($invalidTypes);
|
||||
|
||||
if (1 === \count($keys) && '[]' === substr($keys[0], -2)) {
|
||||
throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but one of the elements is of type "%s".', $option, $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), $keys[0]));
|
||||
throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but one of the elements is of type "%s".', $this->formatOptions([$option]), $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), $keys[0]));
|
||||
}
|
||||
|
||||
throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but is of type "%s".', $option, $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), implode('|', array_keys($invalidTypes))));
|
||||
throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but is of type "%s".', $this->formatOptions([$option]), $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), implode('|', array_keys($invalidTypes))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -989,7 +993,7 @@ class OptionsResolver implements Options
|
||||
if ($deprecationMessage instanceof \Closure) {
|
||||
// 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.', implode('", "', array_keys($this->calling))));
|
||||
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling))));
|
||||
}
|
||||
|
||||
$this->calling[$option] = true;
|
||||
@ -1012,7 +1016,7 @@ class OptionsResolver implements Options
|
||||
// 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.', implode('", "', array_keys($this->calling))));
|
||||
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
|
||||
@ -1195,4 +1199,20 @@ class OptionsResolver implements Options
|
||||
|
||||
return implode(', ', $values);
|
||||
}
|
||||
|
||||
private function formatOptions(array $options): string
|
||||
{
|
||||
if ($this->parentsOptions) {
|
||||
$prefix = array_shift($this->parentsOptions);
|
||||
if ($this->parentsOptions) {
|
||||
$prefix .= sprintf('[%s]', implode('][', $this->parentsOptions));
|
||||
}
|
||||
|
||||
$options = array_map(static function (string $option) use ($prefix): string {
|
||||
return sprintf('%s[%s]', $prefix, $option);
|
||||
}, $options);
|
||||
}
|
||||
|
||||
return implode('", "', $options);
|
||||
}
|
||||
}
|
||||
|
@ -1993,7 +1993,7 @@ class OptionsResolverTest extends TestCase
|
||||
public function testFailsIfUndefinedNestedOption()
|
||||
{
|
||||
$this->expectException('Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException');
|
||||
$this->expectExceptionMessage('The option "foo" does not exist. Defined options are: "host", "port".');
|
||||
$this->expectExceptionMessage('The option "database[foo]" does not exist. Defined options are: "host", "port".');
|
||||
$this->resolver->setDefaults([
|
||||
'name' => 'default',
|
||||
'database' => function (OptionsResolver $resolver) {
|
||||
@ -2008,7 +2008,7 @@ class OptionsResolverTest extends TestCase
|
||||
public function testFailsIfMissingRequiredNestedOption()
|
||||
{
|
||||
$this->expectException('Symfony\Component\OptionsResolver\Exception\MissingOptionsException');
|
||||
$this->expectExceptionMessage('The required option "host" is missing.');
|
||||
$this->expectExceptionMessage('The required option "database[host]" is missing.');
|
||||
$this->resolver->setDefaults([
|
||||
'name' => 'default',
|
||||
'database' => function (OptionsResolver $resolver) {
|
||||
@ -2023,7 +2023,7 @@ class OptionsResolverTest extends TestCase
|
||||
public function testFailsIfInvalidTypeNestedOption()
|
||||
{
|
||||
$this->expectException('Symfony\Component\OptionsResolver\Exception\InvalidOptionsException');
|
||||
$this->expectExceptionMessage('The option "logging" with value null is expected to be of type "bool", but is of type "NULL".');
|
||||
$this->expectExceptionMessage('The option "database[logging]" with value null is expected to be of type "bool", but is of type "NULL".');
|
||||
$this->resolver->setDefaults([
|
||||
'name' => 'default',
|
||||
'database' => function (OptionsResolver $resolver) {
|
||||
|
Reference in New Issue
Block a user