feature #35400 [RFC][DX][OptionsResolver] Allow setting info message per option (yceruto)
This PR was merged into the 5.1-dev branch.
Discussion
----------
[RFC][DX][OptionsResolver] Allow setting info message per option
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| Tickets | -
| License | MIT
| Doc PR | TODO
This is a DX proposal that will help in debugging/errors to better understand the meaning of one defined option.
This is how you'd use it:
```php
$resolver = new OptionsResolver();
$resolver->setDefined('date');
$resolver->setAllowedTypes('date', \DateTime::class);
$resolver->setInfo('date', 'A future date'); // <-- NEW
// ...
```
This information may be useful for those options where their name cannot be intuitive enough, or their purpose is too complex. Here is an example (based on the example above):
```php
// ...
$resolver->setAllowedValues('date', static function ($value): bool {
return $value >= new \DateTime('now');
});
```
So, if you introduce a date value that does not match the criteria, you will get this error message:
**Before:**
```
The option "date" with value DateTime is invalid.
```
Note that the allowed value is not printable in this case, hence the error message cannot be clear at all.
**After:**
```
The option "date" with value DateTime is invalid. Info: A future date.
```
Although a more accurate error message can be triggered within the `\Closure` if desired.
Also you'll see this info message on `debug:form` command (see tests), then you have in advance the informative description of any option.
What do you think?
Commits
-------
0477a06d8a
Allow setting one info message per option
This commit is contained in:
commit
9eb7cb1b7b
@ -108,7 +108,15 @@ abstract class Descriptor implements DescriptorInterface
|
||||
|
||||
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),
|
||||
];
|
||||
|
@ -73,6 +73,7 @@ class JsonDescriptor extends Descriptor
|
||||
}
|
||||
}
|
||||
$map += [
|
||||
'info' => 'info',
|
||||
'required' => 'required',
|
||||
'default' => 'default',
|
||||
'allowed_types' => 'allowedTypes',
|
||||
|
@ -114,6 +114,7 @@ class TextDescriptor extends Descriptor
|
||||
];
|
||||
}
|
||||
$map += [
|
||||
'Info' => 'info',
|
||||
'Required' => 'required',
|
||||
'Default' => 'default',
|
||||
'Allowed types' => 'allowedTypes',
|
||||
|
@ -151,6 +151,8 @@ TXT
|
||||
Symfony\Component\Form\Tests\Command\FooType (foo)
|
||||
==================================================
|
||||
|
||||
---------------- -----------%s
|
||||
Info "Info" %s
|
||||
---------------- -----------%s
|
||||
Required true %s
|
||||
---------------- -----------%s
|
||||
@ -209,5 +211,6 @@ class FooType extends AbstractType
|
||||
$resolver->setNormalizer('foo', function (Options $options, $value) {
|
||||
return (string) $value;
|
||||
});
|
||||
$resolver->setInfo('foo', 'Info');
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
Symfony\Component\Form\Extension\Core\Type\ChoiceType (choice_translation_domain)
|
||||
=================================================================================
|
||||
|
||||
---------------- -----------%s
|
||||
Info - %s
|
||||
---------------- -----------%s
|
||||
Required false %s
|
||||
---------------- -----------%s
|
||||
|
@ -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 -
|
||||
--------------------- -----------------------------------
|
||||
--------------------- -----------------------------------
|
||||
|
@ -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
|
||||
|
@ -2,6 +2,8 @@
|
||||
Symfony\Component\Form\Tests\Console\Descriptor\FooType (foo)
|
||||
=============================================================
|
||||
|
||||
---------------- -----------%s
|
||||
Info - %s
|
||||
---------------- -----------%s
|
||||
Required true %s
|
||||
---------------- -----------%s
|
||||
|
@ -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",
|
||||
|
@ -5,6 +5,7 @@ CHANGELOG
|
||||
-----
|
||||
|
||||
* added fluent configuration of options using `OptionResolver::define()`
|
||||
* added `setInfo()` and `getInfo()` methods
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
@ -124,4 +124,18 @@ final class OptionConfigurator
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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 @@ class OptionsResolver implements Options
|
||||
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 @@ class OptionsResolver implements Options
|
||||
|
||||
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 @@ class OptionsResolver implements Options
|
||||
$this->allowedTypes = [];
|
||||
$this->allowedValues = [];
|
||||
$this->deprecated = [];
|
||||
$this->info = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -996,6 +1037,10 @@ class OptionsResolver implements Options
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($this->info[$option])) {
|
||||
$message .= sprintf(' Info: %s.', $this->info[$option]);
|
||||
}
|
||||
|
||||
throw new InvalidOptionsException($message);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,9 @@ namespace Symfony\Component\OptionsResolver\Tests;
|
||||
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 @@ class OptionsResolverTest extends TestCase
|
||||
->normalize(static function (Options $options, $value) {
|
||||
return $value;
|
||||
})
|
||||
->info('info message')
|
||||
;
|
||||
$introspector = new OptionsResolverIntrospector($this->resolver);
|
||||
|
||||
@ -2409,5 +2412,63 @@ class OptionsResolverTest extends TestCase
|
||||
$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')]);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user