Improve the way one can deprecate a Twig callable

This commit is contained in:
Fabien Potencier
2024-09-09 16:17:33 +02:00
parent 247ee93df9
commit 34ae0311d9
11 changed files with 260 additions and 41 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# 3.15.0 (2024-XX-XX)
* n/a
* Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options)
# 3.14.0 (2024-09-09)
+36 -12
View File
@@ -277,29 +277,53 @@ filter: ``('a', 'b', 'foo')``.
Deprecated Filters
~~~~~~~~~~~~~~~~~~
You can mark a filter as being deprecated by setting the ``deprecated`` option
to ``true``. You can also give an alternative filter that replaces the
deprecated one when that makes sense::
.. versionadded:: 3.15
The ``deprecation_info`` option was added in Twig 3.15.
You can mark a filter as being deprecated by setting the ``deprecation_info``
option::
$filter = new \Twig\TwigFilter('obsolete', function () {
// ...
}, ['deprecated' => true, 'alternative' => 'new_one']);
}, ['deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.11', 'new_one')]);
.. versionadded:: 3.11
The ``DeprecatedCallableInfo`` constructor takes the following parameters:
The ``deprecating_package`` option was added in Twig 3.11.
* The Composer package name that defines the filter;
* The version when the filter was deprecated.
You can also set the ``deprecating_package`` option to specify the package that
is deprecating the filter, and ``deprecated`` can be set to the package version
when the filter was deprecated::
Optionally, you can also provide the following parameters about an alternative:
$filter = new \Twig\TwigFilter('obsolete', function () {
// ...
}, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar']);
* The package name that contains the alternative filter;
* The alternative filter name that replaces the deprecated one;
* The package version that added the alternative filter.
When a filter is deprecated, Twig emits a deprecation notice when compiling a
template using it. See :ref:`deprecation-notices` for more information.
.. note::
Before Twig 3.15, you can mark a filter as being deprecated by setting the
``deprecated`` option to ``true``. You can also give an alternative filter
that replaces the deprecated one when that makes sense::
$filter = new \Twig\TwigFilter('obsolete', function () {
// ...
}, ['deprecated' => true, 'alternative' => 'new_one']);
.. versionadded:: 3.11
The ``deprecating_package`` option was added in Twig 3.11.
You can also set the ``deprecating_package`` option to specify the package
that is deprecating the filter, and ``deprecated`` can be set to the
package version when the filter was deprecated::
$filter = new \Twig\TwigFilter('obsolete', function () {
// ...
}, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar']);
Functions
---------
+19
View File
@@ -237,3 +237,22 @@ Environment
After::
$context += $twig->getGlobals();
Functions/Filters/Tests
-----------------------
* The ``deprecated``, ``deprecating_package``, ``alternative`` options on Twig
functions/filters/Tests are deprecated as of Twig 3.15, and will be removed
in Twig 4.0. Use the ``deprecation_info`` option instead:
Before::
$twig->addFunction(new TwigFunction('foo', 'foo', [
'deprecated' => '3.12', 'deprecating_package' => 'twig/twig',
]));
After::
$twig->addFunction(new TwigFunction('foo', 'foo', [
'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12'),
]));
+46 -1
View File
@@ -33,10 +33,35 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
'needs_context' => false,
'needs_charset' => false,
'is_variadic' => false,
'deprecation_info' => null,
'deprecated' => false,
'deprecating_package' => '',
'alternative' => null,
], $options);
if ($this->options['deprecation_info'] && !$this->options['deprecation_info'] instanceof DeprecatedCallableInfo) {
throw new \LogicException(\sprintf('The "deprecation_info" option must be an instance of "%s".', DeprecatedCallableInfo::class));
}
if ($this->options['deprecated']) {
if ($this->options['deprecation_info']) {
throw new \LogicException('When setting the "deprecation_info" option, you need to remove the obsolete deprecated options.');
}
trigger_deprecation('twig/twig', '3.15', 'Using the "deprecated", "deprecating_package", and "alternative" options is deprecated, pass a "deprecation_info" one instead.');
$this->options['deprecation_info'] = new DeprecatedCallableInfo(
$this->options['deprecating_package'],
$this->options['deprecated'],
null,
$this->options['alternative'],
);
}
if ($this->options['deprecation_info']) {
$this->options['deprecation_info']->setName($name);
$this->options['deprecation_info']->setType($this->getType());
}
}
public function __toString(): string
@@ -111,21 +136,41 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
public function isDeprecated(): bool
{
return (bool) $this->options['deprecated'];
return (bool) $this->options['deprecation_info'];
}
public function triggerDeprecation(?string $file = null, ?int $line = null): void
{
$this->options['deprecation_info']->triggerDeprecation($file, $line);
}
/**
* @deprecated since Twig 3.15
*/
public function getDeprecatingPackage(): string
{
trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
return $this->options['deprecating_package'];
}
/**
* @deprecated since Twig 3.15
*/
public function getDeprecatedVersion(): string
{
trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated'];
}
/**
* @deprecated since Twig 3.15
*/
public function getAlternative(): ?string
{
trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
return $this->options['alternative'];
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class DeprecatedCallableInfo
{
private string $type;
private string $name;
public function __construct(
private string $package,
private string $version,
private ?string $altName = null,
private ?string $altPackage = null,
private ?string $altVersion = null,
) {
}
public function setType(string $type): void
{
$this->type = $type;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function triggerDeprecation(?string $file = null, ?int $line = null): void
{
$message = \sprintf('Twig %s "%s" is deprecated', ucfirst($this->type), $this->name);
if ($this->altName) {
$message .= \sprintf('; use "%s"', $this->altName);
if ($this->altPackage) {
$message .= \sprintf(' from the "%s" package', $this->altPackage);
}
if ($this->altVersion) {
$message .= \sprintf(' (available since version %s)', $this->altVersion);
}
$message .= ' instead';
}
if ($file) {
$message .= \sprintf(' in %s', $file);
if ($line) {
$message .= \sprintf(' at line %d', $line);
}
}
$message .= '.';
trigger_deprecation($this->package, $this->version, $message);
}
}
+3 -22
View File
@@ -773,15 +773,8 @@ class ExpressionParser
if ($test->isDeprecated()) {
$stream = $this->parser->getStream();
$message = \sprintf('Twig Test "%s" is deprecated', $test->getName());
if ($test->getAlternative()) {
$message .= \sprintf('. Use "%s" instead', $test->getAlternative());
}
$src = $stream->getSourceContext();
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
trigger_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message);
$test->triggerDeprecation($src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
}
return $test;
@@ -797,14 +790,8 @@ class ExpressionParser
}
if ($function->isDeprecated()) {
$message = \sprintf('Twig Function "%s" is deprecated', $function->getName());
if ($function->getAlternative()) {
$message .= \sprintf('. Use "%s" instead', $function->getAlternative());
}
$src = $this->parser->getStream()->getSourceContext();
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
trigger_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message);
$function->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
}
return $function;
@@ -820,14 +807,8 @@ class ExpressionParser
}
if ($filter->isDeprecated()) {
$message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName());
if ($filter->getAlternative()) {
$message .= \sprintf('. Use "%s" instead', $filter->getAlternative());
}
$src = $this->parser->getStream()->getSourceContext();
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
trigger_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message);
$filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
}
return $filter;
+2 -1
View File
@@ -11,6 +11,7 @@
namespace Twig\Extension;
use Twig\DeprecatedCallableInfo;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
@@ -217,7 +218,7 @@ final class CoreExtension extends AbstractExtension
new TwigFilter('striptags', [self::class, 'striptags']),
new TwigFilter('trim', [self::class, 'trim']),
new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecated' => '3.12', 'deprecating_package' => 'twig/twig']),
new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12')]),
// array helpers
new TwigFilter('join', [self::class, 'join']),
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace Twig\Tests;
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use PHPUnit\Framework\TestCase;
use Twig\DeprecatedCallableInfo;
class DeprecatedCallableInfoTest extends TestCase
{
/**
* @dataProvider provideTestsForTriggerDeprecation
*/
public function testTriggerDeprecation($expected, DeprecatedCallableInfo $info)
{
$info->setType('function');
$info->setName('foo');
$deprecations = [];
try {
set_error_handler(function ($type, $msg) use (&$deprecations) {
if (\E_USER_DEPRECATED === $type) {
$deprecations[] = $msg;
}
return false;
});
$info->triggerDeprecation('foo.twig', 1);
} finally {
restore_error_handler();
}
$this->assertSame([$expected], $deprecations);
}
public static function provideTestsForTriggerDeprecation(): iterable
{
yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1')];
yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated; use "alt_foo" from the "all/bar" package (available since version 12.10) instead in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1', 'alt_foo', 'all/bar', '12.10')];
yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated; use "alt_foo" from the "all/bar" package instead in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1', 'alt_foo', 'all/bar')];
yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated; use "alt_foo" instead in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1', 'alt_foo')];
}
public function testTriggerDeprecationWithoutFileOrLine()
{
$info = new DeprecatedCallableInfo('foo/bar', '1.1');
$info->setType('function');
$info->setName('foo');
$deprecations = [];
try {
set_error_handler(function ($type, $msg) use (&$deprecations) {
if (\E_USER_DEPRECATED === $type) {
$deprecations[] = $msg;
}
return false;
});
$info->triggerDeprecation();
$info->triggerDeprecation('foo.twig');
} finally {
restore_error_handler();
}
$this->assertSame([
'Since foo/bar 1.1: Twig Function "foo" is deprecated.',
'Since foo/bar 1.1: Twig Function "foo" is deprecated in foo.twig.',
], $deprecations);
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
--TEST--
Functions can be deprecated_function
--DEPRECATION--
Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated. Use "not_deprecated_function" instead in index.twig at line 2.
Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated. Use "not_deprecated_function" instead in index.twig at line 4.
Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated; use "not_deprecated_function" instead in index.twig at line 2.
Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated; use "not_deprecated_function" instead in index.twig at line 4.
--TEMPLATE--
{{ deprecated_function() }}
+2 -1
View File
@@ -11,6 +11,7 @@ namespace Twig\Tests;
* file that was distributed with this source code.
*/
use Twig\DeprecatedCallableInfo;
use Twig\Extension\AbstractExtension;
use Twig\Extension\DebugExtension;
use Twig\Extension\SandboxExtension;
@@ -185,7 +186,7 @@ class TwigTestExtension extends AbstractExtension
new TwigFunction('*_path', [$this, 'dynamic_path']),
new TwigFunction('*_foo_*_bar', [$this, 'dynamic_foo']),
new TwigFunction('anon_foo', function ($name) { return '*'.$name.'*'; }),
new TwigFunction('deprecated_function', function () { return 'foo'; }, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar', 'alternative' => 'not_deprecated_function']),
new TwigFunction('deprecated_function', function () { return 'foo'; }, ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.1', 'not_deprecated_function')]),
];
}
+2 -1
View File
@@ -12,6 +12,7 @@ namespace Twig\Tests\Util;
*/
use PHPUnit\Framework\TestCase;
use Twig\DeprecatedCallableInfo;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\TwigFunction;
@@ -25,7 +26,7 @@ class DeprecationCollectorTest extends TestCase
public function testCollect()
{
$twig = new Environment(new ArrayLoader());
$twig->addFunction(new TwigFunction('deprec', [$this, 'deprec'], ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar']));
$twig->addFunction(new TwigFunction('deprec', [$this, 'deprec'], ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.1')]));
$collector = new DeprecationCollector($twig);
$deprecations = $collector->collect(new Iterator());