feature #35871 [Config] Improve the deprecation features by handling package and version (atailouloute)

This PR was merged into the 5.1-dev branch.

Discussion
----------

[Config] Improve the deprecation features by handling package and version

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | yes
| Tickets       | https://github.com/orgs/symfony/projects/1#card-32681032
| License       | MIT
| Doc PR        | TODO

Commits
-------

f4de76dba0 [Config] Improve the deprecation features by handling package and version
This commit is contained in:
Nicolas Grekas 2020-04-01 10:41:36 +02:00
commit 0bec08f0d8
18 changed files with 199 additions and 29 deletions

View File

@ -1,6 +1,13 @@
UPGRADE FROM 5.0 to 5.1
=======================
Config
------
* The signature of method `NodeDefinition::setDeprecated()` has been updated to `NodeDefinition::setDeprecation(string $package, string $version, string $message)`.
* The signature of method `BaseNode::setDeprecated()` has been updated to `BaseNode::setDeprecation(string $package, string $version, string $message)`.
* Passing a null message to `BaseNode::setDeprecated()` to un-deprecate a node is deprecated
Console
-------

View File

@ -1,6 +1,13 @@
UPGRADE FROM 5.x to 6.0
=======================
Config
------
* The signature of method `NodeDefinition::setDeprecated()` has been updated to `NodeDefinition::setDeprecation(string $package, string $version, string $message)`.
* The signature of method `BaseNode::setDeprecated()` has been updated to `BaseNode::setDeprecation(string $package, string $version, string $message)`.
* Passing a null message to `BaseNode::setDeprecated()` to un-deprecate a node is not supported anymore.
Console
-------

View File

@ -1,6 +1,13 @@
CHANGELOG
=========
5.1.0
-----
* updated the signature of method `NodeDefinition::setDeprecated()` to `NodeDefinition::setDeprecation(string $package, string $version, string $message)`
* updated the signature of method `BaseNode::setDeprecated()` to `BaseNode::setDeprecation(string $package, string $version, string $message)`
* deprecated passing a null message to `BaseNode::setDeprecated()` to un-deprecate a node
5.0.0
-----

View File

@ -227,7 +227,8 @@ class ArrayNode extends BaseNode implements PrototypeNodeInterface
}
if ($child->isDeprecated()) {
trigger_deprecation('', '', $child->getDeprecationMessage($name, $this->getPath()));
$deprecation = $child->getDeprecation($name, $this->getPath());
trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']);
}
try {

View File

@ -35,7 +35,7 @@ abstract class BaseNode implements NodeInterface
protected $finalValidationClosures = [];
protected $allowOverwrite = true;
protected $required = false;
protected $deprecationMessage = null;
protected $deprecation = [];
protected $equivalentValues = [];
protected $attributes = [];
protected $pathSeparator;
@ -198,12 +198,41 @@ abstract class BaseNode implements NodeInterface
/**
* Sets this node as deprecated.
*
* @param string $package The name of the composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The deprecation message to use
*
* You can use %node% and %path% placeholders in your message to display,
* respectively, the node name and its complete path.
*/
public function setDeprecated(?string $message)
public function setDeprecated(?string $package/*, string $version, string $message = 'The child node "%node%" at path "%path%" is deprecated.' */)
{
$this->deprecationMessage = $message;
$args = \func_get_args();
if (\func_num_args() < 2) {
trigger_deprecation('symfony/config', '5.1', 'The signature of method "%s()" requires 3 arguments: "string $package, string $version, string $message", not defining them is deprecated.', __METHOD__);
if (!isset($args[0])) {
trigger_deprecation('symfony/config', '5.1', 'Passing a null message to un-deprecate a node is deprecated.');
$this->deprecation = [];
return;
}
$message = (string) $args[0];
$package = $version = '';
} else {
$package = (string) $args[0];
$version = (string) $args[1];
$message = (string) ($args[2] ?? 'The child node "%node%" at path "%path%" is deprecated.');
}
$this->deprecation = [
'package' => $package,
'version' => $version,
'message' => $message,
];
}
/**
@ -249,7 +278,7 @@ abstract class BaseNode implements NodeInterface
*/
public function isDeprecated()
{
return null !== $this->deprecationMessage;
return (bool) $this->deprecation;
}
/**
@ -259,10 +288,27 @@ abstract class BaseNode implements NodeInterface
* @param string $path the path of the node
*
* @return string
*
* @deprecated since Symfony 5.1, use "getDeprecation()" instead.
*/
public function getDeprecationMessage(string $node, string $path)
{
return strtr($this->deprecationMessage, ['%node%' => $node, '%path%' => $path]);
trigger_deprecation('symfony/config', '5.1', 'The "%s()" method is deprecated, use "getDeprecation()" instead.', __METHOD__);
return $this->getDeprecation($node, $path)['message'];
}
/**
* @param string $node The configuration node name
* @param string $path The path of the node
*/
public function getDeprecation(string $node, string $path): array
{
return [
'package' => $this->deprecation['package'] ?? '',
'version' => $this->deprecation['version'] ?? '',
'message' => strtr($this->deprecation['message'] ?? '', ['%node%' => $node, '%path%' => $path]),
];
}
/**

View File

@ -435,10 +435,13 @@ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinition
$node->addEquivalentValue(false, $this->falseEquivalent);
$node->setPerformDeepMerging($this->performDeepMerging);
$node->setRequired($this->required);
$node->setDeprecated($this->deprecationMessage);
$node->setIgnoreExtraKeys($this->ignoreExtraKeys, $this->removeExtraKeys);
$node->setNormalizeKeys($this->normalizeKeys);
if ($this->deprecation) {
$node->setDeprecated($this->deprecation['package'], $this->deprecation['version'], $this->deprecation['message']);
}
if (null !== $this->normalization) {
$node->setNormalizationClosures($this->normalization->before);
$node->setXmlRemappings($this->normalization->remappings);

View File

@ -28,7 +28,7 @@ abstract class NodeDefinition implements NodeParentInterface
protected $defaultValue;
protected $default = false;
protected $required = false;
protected $deprecationMessage = null;
protected $deprecation = [];
protected $merge;
protected $allowEmptyValue = true;
protected $nullEquivalent;
@ -159,14 +159,35 @@ abstract class NodeDefinition implements NodeParentInterface
/**
* Sets the node as deprecated.
*
* @param string $package The name of the composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The deprecation message to use
*
* You can use %node% and %path% placeholders in your message to display,
* respectively, the node name and its complete path.
*
* @return $this
*/
public function setDeprecated(string $message = 'The child node "%node%" at path "%path%" is deprecated.')
public function setDeprecated(/* string $package, string $version, string $message = 'The child node "%node%" at path "%path%" is deprecated.' */)
{
$this->deprecationMessage = $message;
$args = \func_get_args();
if (\func_num_args() < 2) {
trigger_deprecation('symfony/config', '5.1', 'The signature of method "%s()" requires 3 arguments: "string $package, string $version, string $message", not defining them is deprecated.', __METHOD__);
$message = $args[0] ?? 'The child node "%node%" at path "%path%" is deprecated.';
$package = $version = '';
} else {
$package = (string) $args[0];
$version = (string) $args[1];
$message = (string) ($args[2] ?? 'The child node "%node%" at path "%path%" is deprecated.');
}
$this->deprecation = [
'package' => $package,
'version' => $version,
'message' => $message,
];
return $this;
}

View File

@ -54,7 +54,10 @@ class VariableNodeDefinition extends NodeDefinition
$node->addEquivalentValue(true, $this->trueEquivalent);
$node->addEquivalentValue(false, $this->falseEquivalent);
$node->setRequired($this->required);
$node->setDeprecated($this->deprecationMessage);
if ($this->deprecation) {
$node->setDeprecated($this->deprecation['package'], $this->deprecation['version'], $this->deprecation['message']);
}
if (null !== $this->validation) {
$node->setFinalValidationClosures($this->validation->rules);

View File

@ -148,7 +148,8 @@ class XmlReferenceDumper
}
if ($child->isDeprecated()) {
$comments[] = sprintf('Deprecated (%s)', $child->getDeprecationMessage($child->getName(), $node->getPath()));
$deprecation = $child->getDeprecation($child->getName(), $node->getPath());
$comments[] = sprintf('Deprecated (%s)', ($deprecation['package'] || $deprecation['version'] ? "Since {$deprecation['package']} {$deprecation['version']}: " : '').$deprecation['message']);
}
if ($child instanceof EnumNode) {

View File

@ -120,7 +120,8 @@ class YamlReferenceDumper
// deprecated?
if ($node->isDeprecated()) {
$comments[] = sprintf('Deprecated (%s)', $node->getDeprecationMessage($node->getName(), $parentNode ? $parentNode->getPath() : $node->getPath()));
$deprecation = $node->getDeprecation($node->getName(), $parentNode ? $parentNode->getPath() : $node->getPath());
$comments[] = sprintf('Deprecated (%s)', ($deprecation['package'] || $deprecation['version'] ? "Since {$deprecation['package']} {$deprecation['version']}: " : '').$deprecation['message']);
}
// example

View File

@ -12,12 +12,15 @@
namespace Symfony\Component\Config\Tests\Definition;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\ScalarNode;
class ArrayNodeTest extends TestCase
{
use ExpectDeprecationTrait;
public function testNormalizeThrowsExceptionWhenFalseIsNotAllowed()
{
$this->expectException('Symfony\Component\Config\Definition\Exception\InvalidTypeException');
@ -227,10 +230,13 @@ class ArrayNodeTest extends TestCase
public function testSetDeprecated()
{
$childNode = new ArrayNode('foo');
$childNode->setDeprecated('"%node%" is deprecated');
$childNode->setDeprecated('vendor/package', '1.1', '"%node%" is deprecated');
$this->assertTrue($childNode->isDeprecated());
$this->assertSame('"foo" is deprecated', $childNode->getDeprecationMessage($childNode->getName(), $childNode->getPath()));
$deprecation = $childNode->getDeprecation($childNode->getName(), $childNode->getPath());
$this->assertSame('"foo" is deprecated', $deprecation['message']);
$this->assertSame('vendor/package', $deprecation['package']);
$this->assertSame('1.1', $deprecation['version']);
$node = new ArrayNode('root');
$node->addChild($childNode);
@ -256,6 +262,37 @@ class ArrayNodeTest extends TestCase
$this->assertTrue($deprecationTriggered, '->finalize() should trigger if the deprecated node is set');
}
/**
* @group legacy
*/
public function testUnDeprecateANode()
{
$this->expectDeprecation('Since symfony/config 5.1: The signature of method "Symfony\Component\Config\Definition\BaseNode::setDeprecated()" requires 3 arguments: "string $package, string $version, string $message", not defining them is deprecated.');
$this->expectDeprecation('Since symfony/config 5.1: Passing a null message to un-deprecate a node is deprecated.');
$node = new ArrayNode('foo');
$node->setDeprecated('"%node%" is deprecated');
$node->setDeprecated(null);
$this->assertFalse($node->isDeprecated());
}
/**
* @group legacy
*/
public function testSetDeprecatedWithoutPackageAndVersion()
{
$this->expectDeprecation('Since symfony/config 5.1: The signature of method "Symfony\Component\Config\Definition\BaseNode::setDeprecated()" requires 3 arguments: "string $package, string $version, string $message", not defining them is deprecated.');
$node = new ArrayNode('foo');
$node->setDeprecated('"%node%" is deprecated');
$deprecation = $node->getDeprecation($node->getName(), $node->getPath());
$this->assertSame('"foo" is deprecated', $deprecation['message']);
$this->assertSame('', $deprecation['package']);
$this->assertSame('', $deprecation['version']);
}
/**
* @dataProvider getDataWithIncludedExtraKeys
*/

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Config\Tests\Definition\Builder;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
@ -21,6 +22,8 @@ use Symfony\Component\Config\Definition\Processor;
class ArrayNodeDefinitionTest extends TestCase
{
use ExpectDeprecationTrait;
public function testAppendingSomeNode()
{
$parent = new ArrayNodeDefinition('root');
@ -332,13 +335,37 @@ class ArrayNodeDefinitionTest extends TestCase
$node = new ArrayNodeDefinition('root');
$node
->children()
->arrayNode('foo')->setDeprecated('The "%path%" node is deprecated.')->end()
->arrayNode('foo')->setDeprecated('vendor/package', '1.1', 'The "%path%" node is deprecated.')->end()
->end()
;
$deprecatedNode = $node->getNode()->getChildren()['foo'];
$this->assertTrue($deprecatedNode->isDeprecated());
$this->assertSame('The "root.foo" node is deprecated.', $deprecatedNode->getDeprecationMessage($deprecatedNode->getName(), $deprecatedNode->getPath()));
$deprecation = $deprecatedNode->getDeprecation($deprecatedNode->getName(), $deprecatedNode->getPath());
$this->assertSame('The "root.foo" node is deprecated.', $deprecation['message']);
$this->assertSame('vendor/package', $deprecation['package']);
$this->assertSame('1.1', $deprecation['version']);
}
/**
* @group legacy
*/
public function testSetDeprecatedWithoutPackageAndVersion()
{
$this->expectDeprecation('Since symfony/config 5.1: The signature of method "Symfony\Component\Config\Definition\Builder\NodeDefinition::setDeprecated()" requires 3 arguments: "string $package, string $version, string $message", not defining them is deprecated.');
$node = new ArrayNodeDefinition('root');
$node
->children()
->arrayNode('foo')->setDeprecated('The "%path%" node is deprecated.')->end()
->end()
;
$deprecatedNode = $node->getNode()->getChildren()['foo'];
$this->assertTrue($deprecatedNode->isDeprecated());
$deprecation = $deprecatedNode->getDeprecation($deprecatedNode->getName(), $deprecatedNode->getPath());
$this->assertSame('The "root.foo" node is deprecated.', $deprecation['message']);
$this->assertSame('', $deprecation['package']);
$this->assertSame('', $deprecation['version']);
}
public function testCannotBeEmptyOnConcreteNode()

View File

@ -27,11 +27,14 @@ class BooleanNodeDefinitionTest extends TestCase
public function testSetDeprecated()
{
$def = new BooleanNodeDefinition('foo');
$def->setDeprecated('The "%path%" node is deprecated.');
$def->setDeprecated('vendor/package', '1.1', 'The "%path%" node is deprecated.');
$node = $def->getNode();
$this->assertTrue($node->isDeprecated());
$this->assertSame('The "foo" node is deprecated.', $node->getDeprecationMessage($node->getName(), $node->getPath()));
$deprecation = $node->getDeprecation($node->getName(), $node->getPath());
$this->assertSame('The "foo" node is deprecated.', $deprecation['message']);
$this->assertSame('vendor/package', $deprecation['package']);
$this->assertSame('1.1', $deprecation['version']);
}
}

View File

@ -63,11 +63,14 @@ class EnumNodeDefinitionTest extends TestCase
{
$def = new EnumNodeDefinition('foo');
$def->values(['foo', 'bar']);
$def->setDeprecated('The "%path%" node is deprecated.');
$def->setDeprecated('vendor/package', '1.1', 'The "%path%" node is deprecated.');
$node = $def->getNode();
$this->assertTrue($node->isDeprecated());
$this->assertSame('The "foo" node is deprecated.', $def->getNode()->getDeprecationMessage($node->getName(), $node->getPath()));
$deprecation = $def->getNode()->getDeprecation($node->getName(), $node->getPath());
$this->assertSame('The "foo" node is deprecated.', $deprecation['message']);
$this->assertSame('vendor/package', $deprecation['package']);
$this->assertSame('1.1', $deprecation['version']);
}
}

View File

@ -38,8 +38,8 @@ class XmlReferenceDumperTest extends TestCase
return str_replace("\n", PHP_EOL, <<<'EOL'
<!-- Namespace: http://example.org/schema/dic/acme_root -->
<!-- scalar-required: Required -->
<!-- scalar-deprecated: Deprecated (The child node "scalar_deprecated" at path "acme_root" is deprecated.) -->
<!-- scalar-deprecated-with-message: Deprecated (Deprecation custom message for "scalar_deprecated_with_message" at "acme_root") -->
<!-- scalar-deprecated: Deprecated (Since vendor/package 1.1: The child node "scalar_deprecated" at path "acme_root" is deprecated.) -->
<!-- scalar-deprecated-with-message: Deprecated (Since vendor/package 1.1: Deprecation custom message for "scalar_deprecated_with_message" at "acme_root") -->
<!-- enum-with-default: One of "this"; "that" -->
<!-- enum: One of "this"; "that" -->
<config

View File

@ -98,8 +98,8 @@ acme_root:
- elem1
- elem2
scalar_required: ~ # Required
scalar_deprecated: ~ # Deprecated (The child node "scalar_deprecated" at path "acme_root" is deprecated.)
scalar_deprecated_with_message: ~ # Deprecated (Deprecation custom message for "scalar_deprecated_with_message" at "acme_root")
scalar_deprecated: ~ # Deprecated (Since vendor/package 1.1: The child node "scalar_deprecated" at path "acme_root" is deprecated.)
scalar_deprecated_with_message: ~ # Deprecated (Since vendor/package 1.1: Deprecation custom message for "scalar_deprecated_with_message" at "acme_root")
node_with_a_looong_name: ~
enum_with_default: this # One of "this"; "that"
enum: ~ # One of "this"; "that"

View File

@ -44,10 +44,13 @@ class ScalarNodeTest extends TestCase
public function testSetDeprecated()
{
$childNode = new ScalarNode('foo');
$childNode->setDeprecated('"%node%" is deprecated');
$childNode->setDeprecated('vendor/package', '1.1', '"%node%" is deprecated');
$this->assertTrue($childNode->isDeprecated());
$this->assertSame('"foo" is deprecated', $childNode->getDeprecationMessage($childNode->getName(), $childNode->getPath()));
$deprecation = $childNode->getDeprecation($childNode->getName(), $childNode->getPath());
$this->assertSame('"foo" is deprecated', $deprecation['message']);
$this->assertSame('vendor/package', $deprecation['package']);
$this->assertSame('1.1', $deprecation['version']);
$node = new ArrayNode('root');
$node->addChild($childNode);

View File

@ -34,8 +34,8 @@ class ExampleConfiguration implements ConfigurationInterface
->scalarNode('scalar_array_empty')->defaultValue([])->end()
->scalarNode('scalar_array_defaults')->defaultValue(['elem1', 'elem2'])->end()
->scalarNode('scalar_required')->isRequired()->end()
->scalarNode('scalar_deprecated')->setDeprecated()->end()
->scalarNode('scalar_deprecated_with_message')->setDeprecated('Deprecation custom message for "%node%" at "%path%"')->end()
->scalarNode('scalar_deprecated')->setDeprecated('vendor/package', '1.1')->end()
->scalarNode('scalar_deprecated_with_message')->setDeprecated('vendor/package', '1.1', 'Deprecation custom message for "%node%" at "%path%"')->end()
->scalarNode('node_with_a_looong_name')->end()
->enumNode('enum_with_default')->values(['this', 'that'])->defaultValue('this')->end()
->enumNode('enum')->values(['this', 'that'])->end()