diff --git a/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php b/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php index 95058190d8a1..e085795bb51c 100644 --- a/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php +++ b/src/Symfony/Component/Form/Console/Descriptor/Descriptor.php @@ -108,7 +108,15 @@ protected function collectOptions(ResolvedFormTypeInterface $type) protected function getOptionDefinition(OptionsResolver $optionsResolver, string $option) { - $definition = [ + $definition = []; + + if ($info = $optionsResolver->getInfo($option)) { + $definition = [ + 'info' => $info, + ]; + } + + $definition += [ 'required' => $optionsResolver->isRequired($option), 'deprecated' => $optionsResolver->isDeprecated($option), ]; diff --git a/src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php index 4ef4b4a3257b..20f827bed319 100644 --- a/src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Component/Form/Console/Descriptor/JsonDescriptor.php @@ -73,6 +73,7 @@ protected function describeOption(OptionsResolver $optionsResolver, array $optio } } $map += [ + 'info' => 'info', 'required' => 'required', 'default' => 'default', 'allowed_types' => 'allowedTypes', diff --git a/src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php b/src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php index 6dc9ac48ac1c..17df85f0fa5b 100644 --- a/src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Component/Form/Console/Descriptor/TextDescriptor.php @@ -114,6 +114,7 @@ protected function describeOption(OptionsResolver $optionsResolver, array $optio ]; } $map += [ + 'Info' => 'info', 'Required' => 'required', 'Default' => 'default', 'Allowed types' => 'allowedTypes', diff --git a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php index 89334666df85..e59b3108eec0 100644 --- a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php @@ -151,6 +151,8 @@ public function testDebugCustomFormTypeOption() Symfony\Component\Form\Tests\Command\FooType (foo) ================================================== + ---------------- -----------%s + Info "Info" %s ---------------- -----------%s Required true %s ---------------- -----------%s @@ -209,5 +211,6 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setNormalizer('foo', function (Options $options, $value) { return (string) $value; }); + $resolver->setInfo('foo', 'Info'); } } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/default_option_with_normalizer.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/default_option_with_normalizer.txt index d02b0c02ae3a..4a226f576d60 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/default_option_with_normalizer.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/default_option_with_normalizer.txt @@ -2,6 +2,8 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (choice_translation_domain) ================================================================================= + ---------------- -----------%s + Info - %s ---------------- -----------%s Required false %s ---------------- -----------%s diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/deprecated_option.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/deprecated_option.txt index e607f20b7375..b7edd974bb37 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/deprecated_option.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/deprecated_option.txt @@ -5,6 +5,8 @@ Symfony\Component\Form\Tests\Console\Descriptor\FooType (bar) Deprecated true --------------------- ----------------------------------- Deprecation message "The option "bar" is deprecated." + --------------------- ----------------------------------- + Info - --------------------- ----------------------------------- Required false --------------------- ----------------------------------- @@ -15,4 +17,4 @@ Symfony\Component\Form\Tests\Console\Descriptor\FooType (bar) Allowed values - --------------------- ----------------------------------- Normalizers - - --------------------- ----------------------------------- \ No newline at end of file + --------------------- ----------------------------------- diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/overridden_option_with_default_closures.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/overridden_option_with_default_closures.txt index b184d75a448e..043a1dbc2739 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/overridden_option_with_default_closures.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/overridden_option_with_default_closures.txt @@ -2,6 +2,8 @@ Symfony\Component\Form\Tests\Console\Descriptor\FooType (empty_data) ==================================================================== ---------------- ----------------------%s + Info - %s + ---------------- ----------------------%s Required false %s ---------------- ----------------------%s Default Value: null %s diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/required_option_with_allowed_values.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/required_option_with_allowed_values.txt index b42c10f5bd79..fd2f8f3f19c2 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/required_option_with_allowed_values.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/required_option_with_allowed_values.txt @@ -2,6 +2,8 @@ Symfony\Component\Form\Tests\Console\Descriptor\FooType (foo) ============================================================= + ---------------- -----------%s + Info - %s ---------------- -----------%s Required true %s ---------------- -----------%s diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 7c24ccf088dd..92b713801064 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -20,7 +20,7 @@ "symfony/deprecation-contracts": "^2.1", "symfony/event-dispatcher": "^4.4|^5.0", "symfony/intl": "^4.4|^5.0", - "symfony/options-resolver": "^5.0", + "symfony/options-resolver": "^5.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", "symfony/property-access": "^5.0", diff --git a/src/Symfony/Component/OptionsResolver/CHANGELOG.md b/src/Symfony/Component/OptionsResolver/CHANGELOG.md index 9f62de110f21..54c7951d71d9 100644 --- a/src/Symfony/Component/OptionsResolver/CHANGELOG.md +++ b/src/Symfony/Component/OptionsResolver/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * added fluent configuration of options using `OptionResolver::define()` + * added `setInfo()` and `getInfo()` methods 5.0.0 ----- diff --git a/src/Symfony/Component/OptionsResolver/OptionConfigurator.php b/src/Symfony/Component/OptionsResolver/OptionConfigurator.php index 085be8907fee..0639ac5c79ef 100644 --- a/src/Symfony/Component/OptionsResolver/OptionConfigurator.php +++ b/src/Symfony/Component/OptionsResolver/OptionConfigurator.php @@ -124,4 +124,18 @@ public function required(): self return $this; } + + /** + * Sets an info message for an option. + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function info(string $info): self + { + $this->resolver->setInfo($this->name, $info); + + return $this; + } } diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index e0d1bcaf2b31..247f7415c8cd 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -71,6 +71,11 @@ class OptionsResolver implements Options */ private $allowedTypes = []; + /** + * A list of info messages for each option. + */ + private $info = []; + /** * A list of closures for evaluating lazy options. */ @@ -715,6 +720,41 @@ public function define(string $option): OptionConfigurator return new OptionConfigurator($option, $this); } + /** + * Sets an info message for an option. + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setInfo(string $option, string $info): self + { + if ($this->locked) { + throw new AccessException('The Info message cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + $this->info[$option] = $info; + + return $this; + } + + /** + * Gets the info message for an option. + */ + public function getInfo(string $option): ?string + { + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + return $this->info[$option] ?? null; + } + /** * Removes the option with the given name. * @@ -734,7 +774,7 @@ public function remove($optionNames) foreach ((array) $optionNames as $option) { unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]); - unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]); + unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option], $this->info[$option]); } return $this; @@ -763,6 +803,7 @@ public function clear() $this->allowedTypes = []; $this->allowedValues = []; $this->deprecated = []; + $this->info = []; return $this; } @@ -996,6 +1037,10 @@ public function offsetGet($option, bool $triggerDeprecation = true) ); } + if (isset($this->info[$option])) { + $message .= sprintf(' Info: %s.', $this->info[$option]); + } + throw new InvalidOptionsException($message); } } diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 52bbb812ed4e..339fae687743 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -14,7 +14,9 @@ use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector; +use Symfony\Component\OptionsResolver\Exception\AccessException; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -2399,6 +2401,7 @@ public function testResolveOptionsDefinedByOptionConfigurator() ->normalize(static function (Options $options, $value) { return $value; }) + ->info('info message') ; $introspector = new OptionsResolverIntrospector($this->resolver); @@ -2409,5 +2412,63 @@ public function testResolveOptionsDefinedByOptionConfigurator() $this->assertSame(['string', 'bool'], $introspector->getAllowedTypes('foo')); $this->assertSame(['bar', 'zab'], $introspector->getAllowedValues('foo')); $this->assertCount(1, $introspector->getNormalizers('foo')); + $this->assertSame('info message', $this->resolver->getInfo('foo')); + } + + public function testGetInfo() + { + $info = 'The option info message'; + $this->resolver->setDefined('foo'); + $this->resolver->setInfo('foo', $info); + + $this->assertSame($info, $this->resolver->getInfo('foo')); + } + + public function testSetInfoOnNormalization() + { + $this->expectException(AccessException::class); + $this->expectExceptionMessage('The Info message cannot be set from a lazy option or normalizer.'); + + $this->resolver->setDefined('foo'); + $this->resolver->setNormalizer('foo', static function (Options $options, $value) { + $options->setInfo('foo', 'Info'); + }); + + $this->resolver->resolve(['foo' => 'bar']); + } + + public function testSetInfoOnUndefinedOption() + { + $this->expectException(UndefinedOptionsException::class); + $this->expectExceptionMessage('The option "bar" does not exist. Defined options are: "foo".'); + + $this->resolver->setDefined('foo'); + $this->resolver->setInfo('bar', 'The option info message'); + } + + public function testGetInfoOnUndefinedOption2() + { + $this->expectException(UndefinedOptionsException::class); + $this->expectExceptionMessage('The option "bar" does not exist. Defined options are: "foo".'); + + $this->resolver->setDefined('foo'); + $this->resolver->getInfo('bar'); + } + + public function testInfoOnInvalidValue() + { + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The option "expires" with value DateTime is invalid. Info: A future date time.'); + + $this->resolver + ->setRequired('expires') + ->setInfo('expires', 'A future date time') + ->setAllowedTypes('expires', \DateTime::class) + ->setAllowedValues('expires', static function ($value) { + return $value >= new \DateTime('now'); + }) + ; + + $this->resolver->resolve(['expires' => new \DateTime('-1 hour')]); } }