From a981fc3b502fe775d9067ddfa0da3c20cfa434ab Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Thu, 22 Aug 2019 18:11:41 +0200 Subject: [PATCH] [OptionsResolver] Display full nested options hierarchy in exceptions --- .../OptionsResolver/OptionsResolver.php | 56 +++++++++++++------ .../Tests/OptionsResolverTest.php | 6 +- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index f47aef128451a..837f7536f01a2 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -103,6 +103,8 @@ class OptionsResolver implements Options */ private $locked = false; + private $parentsOptions = []; + private static $typeAliases = [ 'boolean' => 'bool', 'integer' => 'int', @@ -423,7 +425,7 @@ public function setDeprecated(string $option, $deprecationMessage = 'The option } 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 @@ public function setNormalizer($option, \Closure $normalizer) } 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 @@ public function addNormalizer(string $option, \Closure $normalizer, bool $forceP } 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 @@ public function setAllowedValues($option, $allowedValues) } 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 @@ public function addAllowedValues($option, $allowedValues) } 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 @@ public function setAllowedTypes($option, $allowedTypes) } 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 @@ public function addAllowedTypes($option, $allowedTypes) } 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 @@ public function resolve(array $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 @@ public function resolve(array $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 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/) // 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 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/) 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 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/) // 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 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/) $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 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/) 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 @@ public function offsetGet($option/*, bool $triggerDeprecation = true*/) // 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 @@ private function formatValues(array $values): string 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); + } } diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 43ebf408e90b0..dc2aba7424677 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -1993,7 +1993,7 @@ public function testIsNestedOption() 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 @@ public function testFailsIfUndefinedNestedOption() 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 @@ public function testFailsIfMissingRequiredNestedOption() 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) {