Skip to content
Permalink
Browse files

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

0477a06 Allow setting one info message per option
  • Loading branch information
fabpot committed Feb 10, 2020
2 parents 332fa65 + 0477a06 commit 9eb7cb1b7b57d58454700ed779443f92cf63f2e0
@@ -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),
];
@@ -73,6 +73,7 @@ protected function describeOption(OptionsResolver $optionsResolver, array $optio
}
}
$map += [
'info' => 'info',
'required' => 'required',
'default' => 'default',
'allowed_types' => 'allowedTypes',
@@ -114,6 +114,7 @@ protected function describeOption(OptionsResolver $optionsResolver, array $optio
];
}
$map += [
'Info' => 'info',
'Required' => 'required',
'Default' => 'default',
'Allowed types' => 'allowedTypes',
@@ -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');
}
}
@@ -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 @@ 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;
}
}
@@ -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);
}
}
@@ -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')]);
}
}

0 comments on commit 9eb7cb1

Please sign in to comment.
You can’t perform that action at this time.