Skip to content

Commit 1942f69

Browse files
committed
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
2 parents ccf1473 + 44f8db9 commit 1942f69

File tree

4 files changed

+122
-1
lines changed

4 files changed

+122
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* added fluent configuration of options using `OptionResolver::define()`
8+
* added `setInfo()` and `getInfo()` methods
89

910
5.0.0
1011
-----

OptionConfigurator.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,18 @@ public function required(): self
124124

125125
return $this;
126126
}
127+
128+
/**
129+
* Sets an info message for an option.
130+
*
131+
* @return $this
132+
*
133+
* @throws AccessException If called from a lazy option or normalizer
134+
*/
135+
public function info(string $info): self
136+
{
137+
$this->resolver->setInfo($this->name, $info);
138+
139+
return $this;
140+
}
127141
}

OptionsResolver.php

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ class OptionsResolver implements Options
7171
*/
7272
private $allowedTypes = [];
7373

74+
/**
75+
* A list of info messages for each option.
76+
*/
77+
private $info = [];
78+
7479
/**
7580
* A list of closures for evaluating lazy options.
7681
*/
@@ -715,6 +720,41 @@ public function define(string $option): OptionConfigurator
715720
return new OptionConfigurator($option, $this);
716721
}
717722

723+
/**
724+
* Sets an info message for an option.
725+
*
726+
* @return $this
727+
*
728+
* @throws UndefinedOptionsException If the option is undefined
729+
* @throws AccessException If called from a lazy option or normalizer
730+
*/
731+
public function setInfo(string $option, string $info): self
732+
{
733+
if ($this->locked) {
734+
throw new AccessException('The Info message cannot be set from a lazy option or normalizer.');
735+
}
736+
737+
if (!isset($this->defined[$option])) {
738+
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined))));
739+
}
740+
741+
$this->info[$option] = $info;
742+
743+
return $this;
744+
}
745+
746+
/**
747+
* Gets the info message for an option.
748+
*/
749+
public function getInfo(string $option): ?string
750+
{
751+
if (!isset($this->defined[$option])) {
752+
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined))));
753+
}
754+
755+
return $this->info[$option] ?? null;
756+
}
757+
718758
/**
719759
* Removes the option with the given name.
720760
*
@@ -734,7 +774,7 @@ public function remove($optionNames)
734774

735775
foreach ((array) $optionNames as $option) {
736776
unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]);
737-
unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]);
777+
unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option], $this->info[$option]);
738778
}
739779

740780
return $this;
@@ -763,6 +803,7 @@ public function clear()
763803
$this->allowedTypes = [];
764804
$this->allowedValues = [];
765805
$this->deprecated = [];
806+
$this->info = [];
766807

767808
return $this;
768809
}
@@ -996,6 +1037,10 @@ public function offsetGet($option, bool $triggerDeprecation = true)
9961037
);
9971038
}
9981039

1040+
if (isset($this->info[$option])) {
1041+
$message .= sprintf(' Info: %s.', $this->info[$option]);
1042+
}
1043+
9991044
throw new InvalidOptionsException($message);
10001045
}
10011046
}

Tests/OptionsResolverTest.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
use PHPUnit\Framework\Assert;
1515
use PHPUnit\Framework\TestCase;
1616
use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector;
17+
use Symfony\Component\OptionsResolver\Exception\AccessException;
1718
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
19+
use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
1820
use Symfony\Component\OptionsResolver\Options;
1921
use Symfony\Component\OptionsResolver\OptionsResolver;
2022

@@ -2399,6 +2401,7 @@ public function testResolveOptionsDefinedByOptionConfigurator()
23992401
->normalize(static function (Options $options, $value) {
24002402
return $value;
24012403
})
2404+
->info('info message')
24022405
;
24032406
$introspector = new OptionsResolverIntrospector($this->resolver);
24042407

@@ -2409,5 +2412,63 @@ public function testResolveOptionsDefinedByOptionConfigurator()
24092412
$this->assertSame(['string', 'bool'], $introspector->getAllowedTypes('foo'));
24102413
$this->assertSame(['bar', 'zab'], $introspector->getAllowedValues('foo'));
24112414
$this->assertCount(1, $introspector->getNormalizers('foo'));
2415+
$this->assertSame('info message', $this->resolver->getInfo('foo'));
2416+
}
2417+
2418+
public function testGetInfo()
2419+
{
2420+
$info = 'The option info message';
2421+
$this->resolver->setDefined('foo');
2422+
$this->resolver->setInfo('foo', $info);
2423+
2424+
$this->assertSame($info, $this->resolver->getInfo('foo'));
2425+
}
2426+
2427+
public function testSetInfoOnNormalization()
2428+
{
2429+
$this->expectException(AccessException::class);
2430+
$this->expectExceptionMessage('The Info message cannot be set from a lazy option or normalizer.');
2431+
2432+
$this->resolver->setDefined('foo');
2433+
$this->resolver->setNormalizer('foo', static function (Options $options, $value) {
2434+
$options->setInfo('foo', 'Info');
2435+
});
2436+
2437+
$this->resolver->resolve(['foo' => 'bar']);
2438+
}
2439+
2440+
public function testSetInfoOnUndefinedOption()
2441+
{
2442+
$this->expectException(UndefinedOptionsException::class);
2443+
$this->expectExceptionMessage('The option "bar" does not exist. Defined options are: "foo".');
2444+
2445+
$this->resolver->setDefined('foo');
2446+
$this->resolver->setInfo('bar', 'The option info message');
2447+
}
2448+
2449+
public function testGetInfoOnUndefinedOption2()
2450+
{
2451+
$this->expectException(UndefinedOptionsException::class);
2452+
$this->expectExceptionMessage('The option "bar" does not exist. Defined options are: "foo".');
2453+
2454+
$this->resolver->setDefined('foo');
2455+
$this->resolver->getInfo('bar');
2456+
}
2457+
2458+
public function testInfoOnInvalidValue()
2459+
{
2460+
$this->expectException(InvalidOptionsException::class);
2461+
$this->expectExceptionMessage('The option "expires" with value DateTime is invalid. Info: A future date time.');
2462+
2463+
$this->resolver
2464+
->setRequired('expires')
2465+
->setInfo('expires', 'A future date time')
2466+
->setAllowedTypes('expires', \DateTime::class)
2467+
->setAllowedValues('expires', static function ($value) {
2468+
return $value >= new \DateTime('now');
2469+
})
2470+
;
2471+
2472+
$this->resolver->resolve(['expires' => new \DateTime('-1 hour')]);
24122473
}
24132474
}

0 commit comments

Comments
 (0)