diff --git a/CHANGELOG b/CHANGELOG index 24fc6e7d364..1ce6a6f0b36 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.11.0 (2024-XX-XX) + * Add the possibility to deprecate attributes and nodes on `Node` * Mark `ConstantExpression` as being `@final` * Add the `find` filter * Fix optimizer mode validation in `OptimizerNodeVisitor` diff --git a/src/Node/NameDeprecation.php b/src/Node/NameDeprecation.php new file mode 100644 index 00000000000..63ab285761a --- /dev/null +++ b/src/Node/NameDeprecation.php @@ -0,0 +1,46 @@ + + */ +class NameDeprecation +{ + private $package; + private $version; + private $newName; + + public function __construct(string $package = '', string $version = '', string $newName = '') + { + $this->package = $package; + $this->version = $version; + $this->newName = $newName; + } + + public function getPackage(): string + { + return $this->package; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getNewName(): string + { + return $this->newName; + } +} diff --git a/src/Node/Node.php b/src/Node/Node.php index e0e473e8cc4..7e4c878423a 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -30,6 +30,10 @@ class Node implements \Countable, \IteratorAggregate protected $tag; private $sourceContext; + /** @var array */ + private $nodeNameDeprecations = []; + /** @var array */ + private $attributeNameDeprecations = []; /** * @param array $nodes An array of named nodes @@ -109,14 +113,37 @@ public function getAttribute(string $name) throw new \LogicException(\sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class)); } + if (isset($this->attributeNameDeprecations[$name])) { + $dep = $this->attributeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated, get the "%s" attribute instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated and will throw an exception in 4.0.', $name, static::class); + } + } + return $this->attributes[$name]; } public function setAttribute(string $name, $value): void { + if (isset($this->attributeNameDeprecations[$name])) { + $dep = $this->attributeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated, set the "%s" attribute instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated and with no alternative in 4.0.', $name, static::class); + } + } + $this->attributes[$name] = $value; } + public function deprecateAttribute(string $name, NameDeprecation $dep): void + { + $this->attributeNameDeprecations[$name] = $dep; + } + public function removeAttribute(string $name): void { unset($this->attributes[$name]); @@ -133,11 +160,29 @@ public function getNode(string $name): self throw new \LogicException(\sprintf('Node "%s" does not exist for Node "%s".', $name, static::class)); } + if (isset($this->nodeNameDeprecations[$name])) { + $dep = $this->nodeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated, get the "%s" node instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated and will throw an exception in 4.0.', $name, static::class); + } + } + return $this->nodes[$name]; } public function setNode(string $name, self $node): void { + if (isset($this->nodeNameDeprecations[$name])) { + $dep = $this->nodeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated, set the "%s" node instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated with no alternative in 4.0.', $name, static::class); + } + } + if (null !== $this->sourceContext) { $node->setSourceContext($this->sourceContext); } @@ -149,6 +194,11 @@ public function removeNode(string $name): void unset($this->nodes[$name]); } + public function deprecateNode(string $name, NameDeprecation $dep): void + { + $this->nodeNameDeprecations[$name] = $dep; + } + /** * @return int */ diff --git a/tests/Node/NodeTest.php b/tests/Node/NodeTest.php index 8e82af5b9e9..b8d796d7582 100644 --- a/tests/Node/NodeTest.php +++ b/tests/Node/NodeTest.php @@ -12,10 +12,14 @@ */ use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Twig\Node\NameDeprecation; use Twig\Node\Node; class NodeTest extends TestCase { + use ExpectDeprecationTrait; + public function testToString() { // callable is not a supported type for a Node attribute, but Drupal uses some apparently @@ -23,4 +27,52 @@ public function testToString() $this->assertEquals('Twig\Node\Node(value: \Closure)', (string) $node); } + + /** + * @group legacy + */ + public function testAttributeDeprecationWithoutAlternative() + { + $node = new Node([], ['foo' => false]); + $node->deprecateAttribute('foo', new NameDeprecation('foo/bar', '2.0')); + + $this->expectDeprecation('Since foo/bar 2.0: Getting attribute "foo" on a "Twig\Node\Node" class is deprecated and will throw an exception in 4.0.'); + $node->getAttribute('foo'); + } + + /** + * @group legacy + */ + public function testAttributeDeprecationWithAlternative() + { + $node = new Node([], ['foo' => false]); + $node->deprecateAttribute('foo', new NameDeprecation('foo/bar', '2.0', 'bar')); + + $this->expectDeprecation('Since foo/bar 2.0: Getting attribute "foo" on a "Twig\Node\Node" class is deprecated, get the "bar" attribute instead.'); + $node->getAttribute('foo'); + } + + /** + * @group legacy + */ + public function testNodeDeprecationWithoutAlternative() + { + $node = new Node(['foo' => new Node()], []); + $node->deprecateNode('foo', new NameDeprecation('foo/bar', '2.0')); + + $this->expectDeprecation('Since foo/bar 2.0: Getting node "foo" on a "Twig\Node\Node" class is deprecated and will throw an exception in 4.0.'); + $node->getNode('foo'); + } + + /** + * @group legacy + */ + public function testNodeAttributeDeprecationWithAlternative() + { + $node = new Node(['foo' => new Node()], []); + $node->deprecateNode('foo', new NameDeprecation('foo/bar', '2.0', 'bar')); + + $this->expectDeprecation('Since foo/bar 2.0: Getting node "foo" on a "Twig\Node\Node" class is deprecated, get the "bar" node instead.'); + $node->getNode('foo'); + } }