Add ability to deprecate options

This commit is contained in:
Yonel Ceruto 2018-05-09 12:49:31 -04:00
parent 5abffbbd07
commit f8746ce8bd
3 changed files with 257 additions and 0 deletions

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
4.2.0
-----
* added `setDeprecated` and `isDeprecated` methods
3.4.0
-----

View File

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

View File

@ -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
*/