Skip to content

Commit 356540a

Browse files
committed
[OptionsResolver] Display full nested options hierarchy in exceptions
1 parent 47322db commit 356540a

File tree

2 files changed

+45
-21
lines changed

2 files changed

+45
-21
lines changed

src/Symfony/Component/OptionsResolver/OptionsResolver.php

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ class OptionsResolver implements Options
103103
*/
104104
private $locked = false;
105105

106+
private $parentsOptions = [];
107+
106108
private static $typeAliases = [
107109
'boolean' => 'bool',
108110
'integer' => 'int',
@@ -423,7 +425,7 @@ public function setDeprecated(string $option, $deprecationMessage = 'The option
423425
}
424426

425427
if (!isset($this->defined[$option])) {
426-
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist, defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
428+
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist, defined options are: "%s".', $this->formatOptionsForAnException($option), implode('", "', array_keys($this->defined))));
427429
}
428430

429431
if (!\is_string($deprecationMessage) && !$deprecationMessage instanceof \Closure) {
@@ -481,7 +483,7 @@ public function setNormalizer($option, \Closure $normalizer)
481483
}
482484

483485
if (!isset($this->defined[$option])) {
484-
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
486+
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptionsForAnException($option), implode('", "', array_keys($this->defined))));
485487
}
486488

487489
$this->normalizers[$option] = [$normalizer];
@@ -526,7 +528,7 @@ public function addNormalizer(string $option, \Closure $normalizer, bool $forceP
526528
}
527529

528530
if (!isset($this->defined[$option])) {
529-
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
531+
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptionsForAnException($option), implode('", "', array_keys($this->defined))));
530532
}
531533

532534
if ($forcePrepend) {
@@ -569,7 +571,7 @@ public function setAllowedValues($option, $allowedValues)
569571
}
570572

571573
if (!isset($this->defined[$option])) {
572-
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
574+
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptionsForAnException($option), implode('", "', array_keys($this->defined))));
573575
}
574576

575577
$this->allowedValues[$option] = \is_array($allowedValues) ? $allowedValues : [$allowedValues];
@@ -610,7 +612,7 @@ public function addAllowedValues($option, $allowedValues)
610612
}
611613

612614
if (!isset($this->defined[$option])) {
613-
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
615+
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptionsForAnException($option), implode('", "', array_keys($this->defined))));
614616
}
615617

616618
if (!\is_array($allowedValues)) {
@@ -651,7 +653,7 @@ public function setAllowedTypes($option, $allowedTypes)
651653
}
652654

653655
if (!isset($this->defined[$option])) {
654-
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
656+
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptionsForAnException($option), implode('", "', array_keys($this->defined))));
655657
}
656658

657659
$this->allowedTypes[$option] = (array) $allowedTypes;
@@ -686,7 +688,7 @@ public function addAllowedTypes($option, $allowedTypes)
686688
}
687689

688690
if (!isset($this->defined[$option])) {
689-
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
691+
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptionsForAnException($option), implode('", "', array_keys($this->defined))));
690692
}
691693

692694
if (!isset($this->allowedTypes[$option])) {
@@ -793,7 +795,7 @@ public function resolve(array $options = [])
793795
ksort($clone->defined);
794796
ksort($diff);
795797

796-
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))));
798+
throw new UndefinedOptionsException(sprintf((\count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Defined options are: "%s".', $this->formatOptionsForAnException(array_keys($diff)), implode('", "', array_keys($clone->defined))));
797799
}
798800

799801
// Override options set by the user
@@ -809,7 +811,7 @@ public function resolve(array $options = [])
809811
if (\count($diff) > 0) {
810812
ksort($diff);
811813

812-
throw new MissingOptionsException(sprintf(\count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', implode('", "', array_keys($diff))));
814+
throw new MissingOptionsException(sprintf(\count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', $this->formatOptionsForAnException(array_keys($diff))));
813815
}
814816

815817
// Lock the container
@@ -860,10 +862,10 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/)
860862
// Check whether the option is set at all
861863
if (!isset($this->defaults[$option]) && !\array_key_exists($option, $this->defaults)) {
862864
if (!isset($this->defined[$option])) {
863-
throw new NoSuchOptionException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
865+
throw new NoSuchOptionException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptionsForAnException($option), implode('", "', array_keys($this->defined))));
864866
}
865867

866-
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));
868+
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->formatOptionsForAnException($option)));
867869
}
868870

869871
$value = $this->defaults[$option];
@@ -872,17 +874,19 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/)
872874
if (isset($this->nested[$option])) {
873875
// If the closure is already being called, we have a cyclic dependency
874876
if (isset($this->calling[$option])) {
875-
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
877+
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptionsForAnException(array_keys($this->calling))));
876878
}
877879

878880
if (!\is_array($value)) {
879-
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)));
881+
throw new InvalidOptionsException(sprintf('The nested option "%s" with value %s is expected to be of type array, but is of type "%s".', $this->formatOptionsForAnException($option), $this->formatValue($value), $this->formatTypeOf($value)));
880882
}
881883

882884
// The following section must be protected from cyclic calls.
883885
$this->calling[$option] = true;
884886
try {
885887
$resolver = new self();
888+
$resolver->parentsOptions = $this->parentsOptions;
889+
$resolver->parentsOptions[] = $option;
886890
foreach ($this->nested[$option] as $closure) {
887891
$closure($resolver, $this);
888892
}
@@ -897,7 +901,7 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/)
897901
// If the closure is already being called, we have a cyclic
898902
// dependency
899903
if (isset($this->calling[$option])) {
900-
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
904+
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptionsForAnException(array_keys($this->calling))));
901905
}
902906

903907
// The following section must be protected from cyclic
@@ -932,10 +936,10 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/)
932936
$keys = array_keys($invalidTypes);
933937

934938
if (1 === \count($keys) && '[]' === substr($keys[0], -2)) {
935-
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]));
939+
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->formatOptionsForAnException($option), $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), $keys[0]));
936940
}
937941

938-
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))));
942+
throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but is of type "%s".', $this->formatOptionsForAnException($option), $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), implode('|', array_keys($invalidTypes))));
939943
}
940944
}
941945

@@ -989,7 +993,7 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/)
989993
if ($deprecationMessage instanceof \Closure) {
990994
// If the closure is already being called, we have a cyclic dependency
991995
if (isset($this->calling[$option])) {
992-
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
996+
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptionsForAnException(array_keys($this->calling))));
993997
}
994998

995999
$this->calling[$option] = true;
@@ -1012,7 +1016,7 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/)
10121016
// If the closure is already being called, we have a cyclic
10131017
// dependency
10141018
if (isset($this->calling[$option])) {
1015-
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
1019+
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptionsForAnException(array_keys($this->calling))));
10161020
}
10171021

10181022
// The following section must be protected from cyclic
@@ -1195,4 +1199,24 @@ private function formatValues(array $values): string
11951199

11961200
return implode(', ', $values);
11971201
}
1202+
1203+
private function formatOptionsForAnException($options): string
1204+
{
1205+
if (!\is_array($options)) {
1206+
$options = [$options];
1207+
}
1208+
1209+
if (!empty($this->parentsOptions)) {
1210+
$prefix = array_shift($this->parentsOptions);
1211+
if (!empty($this->parentsOptions)) {
1212+
$prefix .= sprintf('[%s]', implode('][', $this->parentsOptions));
1213+
}
1214+
1215+
$options = array_map(static function (string $option) use ($prefix): string {
1216+
return sprintf('%s[%s]', $prefix, $option);
1217+
}, $options);
1218+
}
1219+
1220+
return implode('", "', $options);
1221+
}
11981222
}

src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,7 +1993,7 @@ public function testIsNestedOption()
19931993
public function testFailsIfUndefinedNestedOption()
19941994
{
19951995
$this->expectException('Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException');
1996-
$this->expectExceptionMessage('The option "foo" does not exist. Defined options are: "host", "port".');
1996+
$this->expectExceptionMessage('The option "database[foo]" does not exist. Defined options are: "host", "port".');
19971997
$this->resolver->setDefaults([
19981998
'name' => 'default',
19991999
'database' => function (OptionsResolver $resolver) {
@@ -2008,7 +2008,7 @@ public function testFailsIfUndefinedNestedOption()
20082008
public function testFailsIfMissingRequiredNestedOption()
20092009
{
20102010
$this->expectException('Symfony\Component\OptionsResolver\Exception\MissingOptionsException');
2011-
$this->expectExceptionMessage('The required option "host" is missing.');
2011+
$this->expectExceptionMessage('The required option "database[host]" is missing.');
20122012
$this->resolver->setDefaults([
20132013
'name' => 'default',
20142014
'database' => function (OptionsResolver $resolver) {
@@ -2023,7 +2023,7 @@ public function testFailsIfMissingRequiredNestedOption()
20232023
public function testFailsIfInvalidTypeNestedOption()
20242024
{
20252025
$this->expectException('Symfony\Component\OptionsResolver\Exception\InvalidOptionsException');
2026-
$this->expectExceptionMessage('The option "logging" with value null is expected to be of type "bool", but is of type "NULL".');
2026+
$this->expectExceptionMessage('The option "database[logging]" with value null is expected to be of type "bool", but is of type "NULL".');
20272027
$this->resolver->setDefaults([
20282028
'name' => 'default',
20292029
'database' => function (OptionsResolver $resolver) {

0 commit comments

Comments
 (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