Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 76d3589

Browse files
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 | symfony/symfony-docs#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 ------- f8746ce Add ability to deprecate options
2 parents d148fa7 + f8746ce commit 76d3589

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

src/Symfony/Component/OptionsResolver/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
4.2.0
5+
-----
6+
7+
* added `setDeprecated` and `isDeprecated` methods
8+
49
3.4.0
510
-----
611

src/Symfony/Component/OptionsResolver/OptionsResolver.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\OptionsResolver;
1313

1414
use Symfony\Component\OptionsResolver\Exception\AccessException;
15+
use Symfony\Component\OptionsResolver\Exception\InvalidArgumentException;
1516
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
1617
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
1718
use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException;
@@ -75,6 +76,11 @@ class OptionsResolver implements Options
7576
*/
7677
private $calling = array();
7778

79+
/**
80+
* A list of deprecated options.
81+
*/
82+
private $deprecated = array();
83+
7884
/**
7985
* Whether the instance is locked for reading.
8086
*
@@ -348,6 +354,57 @@ public function getDefinedOptions()
348354
return array_keys($this->defined);
349355
}
350356

357+
/**
358+
* Deprecates an option, allowed types or values.
359+
*
360+
* Instead of passing the message, you may also pass a closure with the
361+
* following signature:
362+
*
363+
* function ($value) {
364+
* // ...
365+
* }
366+
*
367+
* The closure receives the value as argument and should return a string.
368+
* Returns an empty string to ignore the option deprecation.
369+
*
370+
* The closure is invoked when {@link resolve()} is called. The parameter
371+
* passed to the closure is the value of the option after validating it
372+
* and before normalizing it.
373+
*
374+
* @param string|\Closure $deprecationMessage
375+
*/
376+
public function setDeprecated(string $option, $deprecationMessage = 'The option "%name%" is deprecated.'): self
377+
{
378+
if ($this->locked) {
379+
throw new AccessException('Options cannot be deprecated from a lazy option or normalizer.');
380+
}
381+
382+
if (!isset($this->defined[$option])) {
383+
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist, defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
384+
}
385+
386+
if (!\is_string($deprecationMessage) && !$deprecationMessage instanceof \Closure) {
387+
throw new InvalidArgumentException(sprintf('Invalid type for deprecation message argument, expected string or \Closure, but got "%s".', \gettype($deprecationMessage)));
388+
}
389+
390+
// ignore if empty string
391+
if ('' === $deprecationMessage) {
392+
return $this;
393+
}
394+
395+
$this->deprecated[$option] = $deprecationMessage;
396+
397+
// Make sure the option is processed
398+
unset($this->resolved[$option]);
399+
400+
return $this;
401+
}
402+
403+
public function isDeprecated(string $option): bool
404+
{
405+
return isset($this->deprecated[$option]);
406+
}
407+
351408
/**
352409
* Sets the normalizer for an option.
353410
*
@@ -620,6 +677,7 @@ public function clear()
620677
$this->normalizers = array();
621678
$this->allowedTypes = array();
622679
$this->allowedValues = array();
680+
$this->deprecated = array();
623681

624682
return $this;
625683
}
@@ -836,6 +894,19 @@ public function offsetGet($option)
836894
}
837895
}
838896

897+
// Check whether the option is deprecated
898+
if (isset($this->deprecated[$option])) {
899+
$deprecationMessage = $this->deprecated[$option];
900+
901+
if ($deprecationMessage instanceof \Closure && !\is_string($deprecationMessage = $deprecationMessage($value))) {
902+
throw new InvalidOptionsException(sprintf('Invalid type for deprecation message, expected string but got "%s", returns an empty string to ignore.', \gettype($deprecationMessage)));
903+
}
904+
905+
if ('' !== $deprecationMessage) {
906+
@trigger_error(strtr($deprecationMessage, array('%name%' => $option)), E_USER_DEPRECATED);
907+
}
908+
}
909+
839910
// Normalize the validated option
840911
if (isset($this->normalizers[$option])) {
841912
// If the closure is already being called, we have a cyclic

src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,187 @@ public function testClearedOptionsAreNotDefined()
450450
$this->assertFalse($this->resolver->isDefined('foo'));
451451
}
452452

453+
/**
454+
* @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException
455+
*/
456+
public function testFailIfSetDeprecatedFromLazyOption()
457+
{
458+
$this->resolver
459+
->setDefault('bar', 'baz')
460+
->setDefault('foo', function (Options $options) {
461+
$options->setDeprecated('bar');
462+
})
463+
->resolve()
464+
;
465+
}
466+
467+
/**
468+
* @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
469+
*/
470+
public function testSetDeprecatedFailsIfUnknownOption()
471+
{
472+
$this->resolver->setDeprecated('foo');
473+
}
474+
475+
/**
476+
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidArgumentException
477+
* @expectedExceptionMessage Invalid type for deprecation message argument, expected string or \Closure, but got "boolean".
478+
*/
479+
public function testSetDeprecatedFailsIfInvalidDeprecationMessageType()
480+
{
481+
$this->resolver
482+
->setDefined('foo')
483+
->setDeprecated('foo', true)
484+
;
485+
}
486+
487+
/**
488+
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidArgumentException
489+
* @expectedExceptionMessage Invalid type for deprecation message, expected string but got "boolean", returns an empty string to ignore.
490+
*/
491+
public function testLazyDeprecationFailsIfInvalidDeprecationMessageType()
492+
{
493+
$this->resolver
494+
->setDefault('foo', true)
495+
->setDeprecated('foo', function ($value) {
496+
return false;
497+
})
498+
;
499+
$this->resolver->resolve();
500+
}
501+
502+
public function testIsDeprecated()
503+
{
504+
$this->resolver
505+
->setDefined('foo')
506+
->setDeprecated('foo')
507+
;
508+
$this->assertTrue($this->resolver->isDeprecated('foo'));
509+
}
510+
511+
public function testIsNotDeprecatedIfEmptyString()
512+
{
513+
$this->resolver
514+
->setDefined('foo')
515+
->setDeprecated('foo', '')
516+
;
517+
$this->assertFalse($this->resolver->isDeprecated('foo'));
518+
}
519+
520+
/**
521+
* @dataProvider provideDeprecationData
522+
*/
523+
public function testDeprecationMessages(\Closure $configureOptions, array $options, ?array $expectedError)
524+
{
525+
error_clear_last();
526+
set_error_handler(function () { return false; });
527+
$e = error_reporting(0);
528+
529+
$configureOptions($this->resolver);
530+
$this->resolver->resolve($options);
531+
532+
error_reporting($e);
533+
restore_error_handler();
534+
535+
$lastError = error_get_last();
536+
unset($lastError['file'], $lastError['line']);
537+
538+
$this->assertSame($expectedError, $lastError);
539+
}
540+
541+
public function provideDeprecationData()
542+
{
543+
yield 'It deprecates an option with default message' => array(
544+
function (OptionsResolver $resolver) {
545+
$resolver
546+
->setDefined(array('foo', 'bar'))
547+
->setDeprecated('foo')
548+
;
549+
},
550+
array('foo' => 'baz'),
551+
array(
552+
'type' => E_USER_DEPRECATED,
553+
'message' => 'The option "foo" is deprecated.',
554+
),
555+
);
556+
557+
yield 'It deprecates an option with custom message' => array(
558+
function (OptionsResolver $resolver) {
559+
$resolver
560+
->setDefined('foo')
561+
->setDefault('bar', function (Options $options) {
562+
return $options['foo'];
563+
})
564+
->setDeprecated('foo', 'The option "foo" is deprecated, use "bar" option instead.')
565+
;
566+
},
567+
array('foo' => 'baz'),
568+
array(
569+
'type' => E_USER_DEPRECATED,
570+
'message' => 'The option "foo" is deprecated, use "bar" option instead.',
571+
),
572+
);
573+
574+
yield 'It deprecates a missing option with default value' => array(
575+
function (OptionsResolver $resolver) {
576+
$resolver
577+
->setDefaults(array('foo' => null, 'bar' => null))
578+
->setDeprecated('foo')
579+
;
580+
},
581+
array('bar' => 'baz'),
582+
array(
583+
'type' => E_USER_DEPRECATED,
584+
'message' => 'The option "foo" is deprecated.',
585+
),
586+
);
587+
588+
yield 'It deprecates allowed type and value' => array(
589+
function (OptionsResolver $resolver) {
590+
$resolver
591+
->setDefault('foo', null)
592+
->setAllowedTypes('foo', array('null', 'string', \stdClass::class))
593+
->setDeprecated('foo', function ($value) {
594+
if ($value instanceof \stdClass) {
595+
return sprintf('Passing an instance of "%s" to option "foo" is deprecated, pass its FQCN instead.', \stdClass::class);
596+
}
597+
598+
return '';
599+
})
600+
;
601+
},
602+
array('foo' => new \stdClass()),
603+
array(
604+
'type' => E_USER_DEPRECATED,
605+
'message' => 'Passing an instance of "stdClass" to option "foo" is deprecated, pass its FQCN instead.',
606+
),
607+
);
608+
609+
yield 'It ignores deprecation for missing option without default value' => array(
610+
function (OptionsResolver $resolver) {
611+
$resolver
612+
->setDefined(array('foo', 'bar'))
613+
->setDeprecated('foo')
614+
;
615+
},
616+
array('bar' => 'baz'),
617+
null,
618+
);
619+
620+
yield 'It ignores deprecation if closure returns an empty string' => array(
621+
function (OptionsResolver $resolver) {
622+
$resolver
623+
->setDefault('foo', null)
624+
->setDeprecated('foo', function ($value) {
625+
return '';
626+
})
627+
;
628+
},
629+
array('foo' => Bar::class),
630+
null,
631+
);
632+
}
633+
453634
/**
454635
* @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
455636
*/

0 commit comments

Comments
 (0)