Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the way one can deprecate a Twig callable #4291

Merged
merged 1 commit into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
48 changes: 36 additions & 12 deletions doc/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------

Expand Down
19 changes: 19 additions & 0 deletions doc/deprecated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
]));
47 changes: 46 additions & 1 deletion src/AbstractTwigCallable.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,35 @@ public function __construct(string $name, $callable = null, array $options = [])
'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.');
fabpot marked this conversation as resolved.
Show resolved Hide resolved
}

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
Expand Down Expand Up @@ -111,21 +136,41 @@ public function isVariadic(): bool

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'];
}

Expand Down
67 changes: 67 additions & 0 deletions src/DeprecatedCallableInfo.php
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
*/
final class DeprecatedCallableInfo
fabpot marked this conversation as resolved.
Show resolved Hide resolved
{
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,
fabpot marked this conversation as resolved.
Show resolved Hide resolved
) {
}

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);
}
}
25 changes: 3 additions & 22 deletions src/ExpressionParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -773,15 +773,8 @@ private function getTest(int $line): TwigTest

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;
Expand All @@ -797,14 +790,8 @@ private function getFunction(string $name, int $line): TwigFunction
}

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;
Expand All @@ -820,14 +807,8 @@ private function getFilter(string $name, int $line): TwigFilter
}

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;
Expand Down
3 changes: 2 additions & 1 deletion src/Extension/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Twig\Extension;

use Twig\DeprecatedCallableInfo;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
Expand Down Expand Up @@ -217,7 +218,7 @@ public function getFilters(): array
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']),
Expand Down
80 changes: 80 additions & 0 deletions tests/DeprecatedCallableInfoTest.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading