feature #27277 [OptionsResolver] Introduce ability to deprecate options, allowed types and values (yceruto)
This PR was merged into the 4.2-dev branch.
Discussion
----------
[OptionsResolver] Introduce ability to deprecate options, allowed types and values
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | #27216
| License | MIT
| Doc PR | https://github.com/symfony/symfony-docs/pull/9859
**Deprecating an option**
```php
$resolver = (new OptionsResolver())
->setDefined(['foo', 'bar'])
->setDeprecated('foo')
;
$resolver->resolve(['foo' => 'baz']); // PHP Deprecated: The option "foo" is deprecated.
```
With custom message:
```php
$resolver = (new OptionsResolver())
->setDefined('foo')
->setDefault('bar', function (Options $options) {
return $options['foo'];
})
->setDeprecated('foo', 'The option "foo" is deprecated, use "bar" option instead.')
;
$resolver->resolve(['foo' => 'baz']); // PHP Deprecated: The option "foo" is deprecated, use "bar" option instead.
$resolver->resolve(['bar' => 'baz']); // OK.
```
**Deprecating allowed types**
```php
$resolver = (new OptionsResolver())
->setDefault('type', null)
->setAllowedTypes('type', ['null', 'string', FormTypeInterface::class])
->setDeprecated('type', function ($value) {
if ($value instanceof FormTypeInterface) {
return sprintf('Passing an instance of "%s" to option "type" is deprecated, pass its FQCN instead.', FormTypeInterface::class);
}
})
;
$resolver->resolve(['type' => new ChoiceType()]); // PHP Deprecated: Passing an instance of "Symfony\Component\Form\FormTypeInterface" to option "type" is deprecated, pass its FQCN instead.
$resolver->resolve(['type' => ChoiceType::class]); // OK.
```
The closure is invoked when `resolve()` is called. The closure must return a string (the deprecation message) or an empty string to ignore the option deprecation.
Multiple types and normalizer:
```php
$resolver = (new OptionsResolver())
->setDefault('percent', 0.0)
->setAllowedTypes('percent', ['null', 'int', 'float'])
->setDeprecated('percent', function ($value) {
if (null === $value) {
return 'Passing "null" to option "percent" is deprecated, pass a float number instead.';
}
if (is_int($value)) {
return sprintf('Passing an integer "%d" to option "percent" is deprecated, pass a float number instead.', $value);
}
})
->setNormalizer('percent', function (Options $options, $value) {
return (float) $value;
})
;
$resolver->resolve(['percent' => null]); // PHP Deprecated: Passing "null" to option "percent" is deprecated, pass a float number instead.
$resolver->resolve(['percent' => 20]); // PHP Deprecated: Passing an integer "20" to option "percent" is deprecated, pass a float number instead.
$resolver->resolve(['percent' => 20.0]); // OK.
```
The parameter passed to the closure is the value of the option after validating it and before normalizing it.
**Deprecating allowed values**
```php
$resolver = (new OptionsResolver())
->setDefault('percent', 0.0)
->setAllowedTypes('percent', 'float')
->setDeprecated('percent', function ($value) {
if ($value < 0) {
return 'Passing a number less than 0 to option "percent" is deprecated.';
}
})
;
$resolver->resolve(['percent' => -50.0]); // PHP Deprecated: Passing a number less than 0 to option "percent" is deprecated.
```
Commits
-------
f8746ce8bd
Add ability to deprecate options
This commit is contained in:
commit
76d3589a44
@ -1,6 +1,11 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
4.2.0
|
||||
-----
|
||||
|
||||
* added `setDeprecated` and `isDeprecated` methods
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\OptionsResolver;
|
||||
|
||||
use Symfony\Component\OptionsResolver\Exception\AccessException;
|
||||
use Symfony\Component\OptionsResolver\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
|
||||
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
|
||||
use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException;
|
||||
@ -75,6 +76,11 @@ class OptionsResolver implements Options
|
||||
*/
|
||||
private $calling = array();
|
||||
|
||||
/**
|
||||
* A list of deprecated options.
|
||||
*/
|
||||
private $deprecated = array();
|
||||
|
||||
/**
|
||||
* Whether the instance is locked for reading.
|
||||
*
|
||||
@ -348,6 +354,57 @@ class OptionsResolver implements Options
|
||||
return array_keys($this->defined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecates an option, allowed types or values.
|
||||
*
|
||||
* Instead of passing the message, you may also pass a closure with the
|
||||
* following signature:
|
||||
*
|
||||
* function ($value) {
|
||||
* // ...
|
||||
* }
|
||||
*
|
||||
* The closure receives the value as argument and should return a string.
|
||||
* Returns an empty string to ignore the option deprecation.
|
||||
*
|
||||
* The closure is invoked when {@link resolve()} is called. The parameter
|
||||
* passed to the closure is the value of the option after validating it
|
||||
* and before normalizing it.
|
||||
*
|
||||
* @param string|\Closure $deprecationMessage
|
||||
*/
|
||||
public function setDeprecated(string $option, $deprecationMessage = 'The option "%name%" is deprecated.'): self
|
||||
{
|
||||
if ($this->locked) {
|
||||
throw new AccessException('Options cannot be deprecated 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".', $option, implode('", "', array_keys($this->defined))));
|
||||
}
|
||||
|
||||
if (!\is_string($deprecationMessage) && !$deprecationMessage instanceof \Closure) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid type for deprecation message argument, expected string or \Closure, but got "%s".', \gettype($deprecationMessage)));
|
||||
}
|
||||
|
||||
// ignore if empty string
|
||||
if ('' === $deprecationMessage) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->deprecated[$option] = $deprecationMessage;
|
||||
|
||||
// Make sure the option is processed
|
||||
unset($this->resolved[$option]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isDeprecated(string $option): bool
|
||||
{
|
||||
return isset($this->deprecated[$option]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the normalizer for an option.
|
||||
*
|
||||
@ -620,6 +677,7 @@ class OptionsResolver implements Options
|
||||
$this->normalizers = array();
|
||||
$this->allowedTypes = array();
|
||||
$this->allowedValues = array();
|
||||
$this->deprecated = array();
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -836,6 +894,19 @@ class OptionsResolver implements Options
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the option is deprecated
|
||||
if (isset($this->deprecated[$option])) {
|
||||
$deprecationMessage = $this->deprecated[$option];
|
||||
|
||||
if ($deprecationMessage instanceof \Closure && !\is_string($deprecationMessage = $deprecationMessage($value))) {
|
||||
throw new InvalidOptionsException(sprintf('Invalid type for deprecation message, expected string but got "%s", returns an empty string to ignore.', \gettype($deprecationMessage)));
|
||||
}
|
||||
|
||||
if ('' !== $deprecationMessage) {
|
||||
@trigger_error(strtr($deprecationMessage, array('%name%' => $option)), E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize the validated option
|
||||
if (isset($this->normalizers[$option])) {
|
||||
// If the closure is already being called, we have a cyclic
|
||||
|
@ -450,6 +450,187 @@ class OptionsResolverTest extends TestCase
|
||||
$this->assertFalse($this->resolver->isDefined('foo'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException
|
||||
*/
|
||||
public function testFailIfSetDeprecatedFromLazyOption()
|
||||
{
|
||||
$this->resolver
|
||||
->setDefault('bar', 'baz')
|
||||
->setDefault('foo', function (Options $options) {
|
||||
$options->setDeprecated('bar');
|
||||
})
|
||||
->resolve()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
|
||||
*/
|
||||
public function testSetDeprecatedFailsIfUnknownOption()
|
||||
{
|
||||
$this->resolver->setDeprecated('foo');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidArgumentException
|
||||
* @expectedExceptionMessage Invalid type for deprecation message argument, expected string or \Closure, but got "boolean".
|
||||
*/
|
||||
public function testSetDeprecatedFailsIfInvalidDeprecationMessageType()
|
||||
{
|
||||
$this->resolver
|
||||
->setDefined('foo')
|
||||
->setDeprecated('foo', true)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidArgumentException
|
||||
* @expectedExceptionMessage Invalid type for deprecation message, expected string but got "boolean", returns an empty string to ignore.
|
||||
*/
|
||||
public function testLazyDeprecationFailsIfInvalidDeprecationMessageType()
|
||||
{
|
||||
$this->resolver
|
||||
->setDefault('foo', true)
|
||||
->setDeprecated('foo', function ($value) {
|
||||
return false;
|
||||
})
|
||||
;
|
||||
$this->resolver->resolve();
|
||||
}
|
||||
|
||||
public function testIsDeprecated()
|
||||
{
|
||||
$this->resolver
|
||||
->setDefined('foo')
|
||||
->setDeprecated('foo')
|
||||
;
|
||||
$this->assertTrue($this->resolver->isDeprecated('foo'));
|
||||
}
|
||||
|
||||
public function testIsNotDeprecatedIfEmptyString()
|
||||
{
|
||||
$this->resolver
|
||||
->setDefined('foo')
|
||||
->setDeprecated('foo', '')
|
||||
;
|
||||
$this->assertFalse($this->resolver->isDeprecated('foo'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDeprecationData
|
||||
*/
|
||||
public function testDeprecationMessages(\Closure $configureOptions, array $options, ?array $expectedError)
|
||||
{
|
||||
error_clear_last();
|
||||
set_error_handler(function () { return false; });
|
||||
$e = error_reporting(0);
|
||||
|
||||
$configureOptions($this->resolver);
|
||||
$this->resolver->resolve($options);
|
||||
|
||||
error_reporting($e);
|
||||
restore_error_handler();
|
||||
|
||||
$lastError = error_get_last();
|
||||
unset($lastError['file'], $lastError['line']);
|
||||
|
||||
$this->assertSame($expectedError, $lastError);
|
||||
}
|
||||
|
||||
public function provideDeprecationData()
|
||||
{
|
||||
yield 'It deprecates an option with default message' => array(
|
||||
function (OptionsResolver $resolver) {
|
||||
$resolver
|
||||
->setDefined(array('foo', 'bar'))
|
||||
->setDeprecated('foo')
|
||||
;
|
||||
},
|
||||
array('foo' => 'baz'),
|
||||
array(
|
||||
'type' => E_USER_DEPRECATED,
|
||||
'message' => 'The option "foo" is deprecated.',
|
||||
),
|
||||
);
|
||||
|
||||
yield 'It deprecates an option with custom message' => array(
|
||||
function (OptionsResolver $resolver) {
|
||||
$resolver
|
||||
->setDefined('foo')
|
||||
->setDefault('bar', function (Options $options) {
|
||||
return $options['foo'];
|
||||
})
|
||||
->setDeprecated('foo', 'The option "foo" is deprecated, use "bar" option instead.')
|
||||
;
|
||||
},
|
||||
array('foo' => 'baz'),
|
||||
array(
|
||||
'type' => E_USER_DEPRECATED,
|
||||
'message' => 'The option "foo" is deprecated, use "bar" option instead.',
|
||||
),
|
||||
);
|
||||
|
||||
yield 'It deprecates a missing option with default value' => array(
|
||||
function (OptionsResolver $resolver) {
|
||||
$resolver
|
||||
->setDefaults(array('foo' => null, 'bar' => null))
|
||||
->setDeprecated('foo')
|
||||
;
|
||||
},
|
||||
array('bar' => 'baz'),
|
||||
array(
|
||||
'type' => E_USER_DEPRECATED,
|
||||
'message' => 'The option "foo" is deprecated.',
|
||||
),
|
||||
);
|
||||
|
||||
yield 'It deprecates allowed type and value' => array(
|
||||
function (OptionsResolver $resolver) {
|
||||
$resolver
|
||||
->setDefault('foo', null)
|
||||
->setAllowedTypes('foo', array('null', 'string', \stdClass::class))
|
||||
->setDeprecated('foo', function ($value) {
|
||||
if ($value instanceof \stdClass) {
|
||||
return sprintf('Passing an instance of "%s" to option "foo" is deprecated, pass its FQCN instead.', \stdClass::class);
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
;
|
||||
},
|
||||
array('foo' => new \stdClass()),
|
||||
array(
|
||||
'type' => E_USER_DEPRECATED,
|
||||
'message' => 'Passing an instance of "stdClass" to option "foo" is deprecated, pass its FQCN instead.',
|
||||
),
|
||||
);
|
||||
|
||||
yield 'It ignores deprecation for missing option without default value' => array(
|
||||
function (OptionsResolver $resolver) {
|
||||
$resolver
|
||||
->setDefined(array('foo', 'bar'))
|
||||
->setDeprecated('foo')
|
||||
;
|
||||
},
|
||||
array('bar' => 'baz'),
|
||||
null,
|
||||
);
|
||||
|
||||
yield 'It ignores deprecation if closure returns an empty string' => array(
|
||||
function (OptionsResolver $resolver) {
|
||||
$resolver
|
||||
->setDefault('foo', null)
|
||||
->setDeprecated('foo', function ($value) {
|
||||
return '';
|
||||
})
|
||||
;
|
||||
},
|
||||
array('foo' => Bar::class),
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
|
||||
*/
|
||||
|
Reference in New Issue
Block a user