mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-01 04:57:23 +00:00
Introduce a way to test Twig callables at compile time
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# 3.15.0 (2024-XX-XX)
|
||||
|
||||
* Add a new `guard` tag that allows to test if some Twig callables are available at compilation time
|
||||
* Allow arrow functions everywhere
|
||||
* Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a `\Closure`)
|
||||
* Add support for triggering deprecations for future operator precedence changes
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
``guard``
|
||||
=========
|
||||
|
||||
.. versionadded:: 3.13
|
||||
|
||||
The ``guard`` tag was added in Twig 3.15.
|
||||
|
||||
The ``guard`` statement checks if some Twig callables are available at
|
||||
**compilation time** to bypass code compilation that would otherwise fail.
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
{% guard function importmap %}
|
||||
{{ importmap('app') }}
|
||||
{% endguard %}
|
||||
|
||||
The first argument is the Twig callable to test: ``filter``, ``function``, or
|
||||
``test``. The second argument is the Twig callable name you want to test.
|
||||
|
||||
You can also generate different code if the callable does not exist:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
{% guard function importmap %}
|
||||
{{ importmap('app') }}
|
||||
{% else %}
|
||||
{# the importmap function doesn't exist, generate fallback code #}
|
||||
{% endguard %}
|
||||
@@ -12,6 +12,7 @@ Tags
|
||||
do
|
||||
embed
|
||||
extends
|
||||
guard
|
||||
flush
|
||||
for
|
||||
from
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
namespace Twig;
|
||||
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\EmptyNode;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
@@ -875,6 +876,9 @@ class ExpressionParser
|
||||
}
|
||||
|
||||
if (!$test) {
|
||||
if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
|
||||
return new TwigTest($name, fn () => '');
|
||||
}
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getTests()));
|
||||
|
||||
@@ -893,6 +897,9 @@ class ExpressionParser
|
||||
private function getFunction(string $name, int $line): TwigFunction
|
||||
{
|
||||
if (!$function = $this->env->getFunction($name)) {
|
||||
if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
|
||||
return new TwigFunction($name, fn () => '');
|
||||
}
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getFunctions()));
|
||||
|
||||
@@ -910,6 +917,9 @@ class ExpressionParser
|
||||
private function getFilter(string $name, int $line): TwigFilter
|
||||
{
|
||||
if (!$filter = $this->env->getFilter($name)) {
|
||||
if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
|
||||
return new TwigFilter($name, fn () => '');
|
||||
}
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getFilters()));
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ use Twig\TokenParser\ExtendsTokenParser;
|
||||
use Twig\TokenParser\FlushTokenParser;
|
||||
use Twig\TokenParser\ForTokenParser;
|
||||
use Twig\TokenParser\FromTokenParser;
|
||||
use Twig\TokenParser\GuardTokenParser;
|
||||
use Twig\TokenParser\IfTokenParser;
|
||||
use Twig\TokenParser\ImportTokenParser;
|
||||
use Twig\TokenParser\IncludeTokenParser;
|
||||
@@ -195,6 +196,7 @@ final class CoreExtension extends AbstractExtension
|
||||
new EmbedTokenParser(),
|
||||
new WithTokenParser(),
|
||||
new DeprecatedTokenParser(),
|
||||
new GuardTokenParser(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -46,12 +46,18 @@ class Parser
|
||||
private $traits;
|
||||
private $embeddedTemplates = [];
|
||||
private $varNameSalt = 0;
|
||||
private $ignoreUnknownTwigCallables = false;
|
||||
|
||||
public function __construct(
|
||||
private Environment $env,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getEnvironment(): Environment
|
||||
{
|
||||
return $this->env;
|
||||
}
|
||||
|
||||
public function getVarName(): string
|
||||
{
|
||||
return \sprintf('__internal_parse_%d', $this->varNameSalt++);
|
||||
@@ -116,6 +122,22 @@ class Parser
|
||||
return $node;
|
||||
}
|
||||
|
||||
public function shouldIgnoreUnknownTwigCallables(): bool
|
||||
{
|
||||
return $this->ignoreUnknownTwigCallables;
|
||||
}
|
||||
|
||||
public function subparseIgnoreUnknownTwigCallables($test, bool $dropNeedle = false): void
|
||||
{
|
||||
$previous = $this->ignoreUnknownTwigCallables;
|
||||
$this->ignoreUnknownTwigCallables = true;
|
||||
try {
|
||||
$this->subparse($test, $dropNeedle);
|
||||
} finally {
|
||||
$this->ignoreUnknownTwigCallables = $previous;
|
||||
}
|
||||
}
|
||||
|
||||
public function subparse($test, bool $dropNeedle = false): Node
|
||||
{
|
||||
$lineno = $this->getCurrentToken()->getLine();
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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\TokenParser;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\EmptyNode;
|
||||
use Twig\Node\Node;
|
||||
use Twig\Node\Nodes;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class GuardTokenParser extends AbstractTokenParser
|
||||
{
|
||||
public function parse(Token $token): Node
|
||||
{
|
||||
$stream = $this->parser->getStream();
|
||||
$typeToken = $stream->expect(Token::NAME_TYPE);
|
||||
if (!in_array($typeToken->getValue(), ['function', 'filter', 'test'])) {
|
||||
throw new SyntaxError(\sprintf('Supported guard types are function, filter and test, "%s" given.', $typeToken->getValue()), $typeToken->getLine(), $stream->getSourceContext());
|
||||
}
|
||||
$method = 'get'.$typeToken->getValue();
|
||||
|
||||
$nameToken = $stream->expect(Token::NAME_TYPE);
|
||||
|
||||
$exists = null !== $this->parser->getEnvironment()->$method($nameToken->getValue());
|
||||
|
||||
$stream->expect(Token::BLOCK_END_TYPE);
|
||||
if ($exists) {
|
||||
$body = $this->parser->subparse([$this, 'decideGuardFork']);
|
||||
} else {
|
||||
$body = new EmptyNode();
|
||||
$this->parser->subparseIgnoreUnknownTwigCallables([$this, 'decideGuardFork']);
|
||||
}
|
||||
$else = new EmptyNode();
|
||||
if ('else' === $stream->next()->getValue()) {
|
||||
$stream->expect(Token::BLOCK_END_TYPE);
|
||||
$else = $this->parser->subparse([$this, 'decideGuardEnd'], true);
|
||||
}
|
||||
$stream->expect(Token::BLOCK_END_TYPE);
|
||||
|
||||
return new Nodes([$exists ? $body : $else]);
|
||||
}
|
||||
|
||||
public function decideGuardFork(Token $token): bool
|
||||
{
|
||||
return $token->test(['else', 'endguard']);
|
||||
}
|
||||
|
||||
public function decideGuardEnd(Token $token): bool
|
||||
{
|
||||
return $token->test(['endguard']);
|
||||
}
|
||||
|
||||
public function getTag(): string
|
||||
{
|
||||
return 'guard';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
--TEST--
|
||||
"guard" creates a compilation time condition on Twig callables availability
|
||||
--TEMPLATE--
|
||||
{% guard filter foobar %}
|
||||
NEVER
|
||||
{{ 'a'|foobar }}
|
||||
{% else -%}
|
||||
The foobar filter doesn't exist
|
||||
{% endguard %}
|
||||
|
||||
{% guard function constant -%}
|
||||
The constant function does exist
|
||||
{% else %}
|
||||
NEVER
|
||||
{% endguard %}
|
||||
--DATA--
|
||||
return []
|
||||
--EXPECT--
|
||||
The foobar filter doesn't exist
|
||||
|
||||
The constant function does exist
|
||||
@@ -0,0 +1,12 @@
|
||||
--TEST--
|
||||
"guard" creates a compilation time condition on Twig callables availability
|
||||
--TEMPLATE--
|
||||
{% guard function foobar %}
|
||||
{{ foobar() }}
|
||||
{% else %}
|
||||
{{ foobar() }}
|
||||
{% endguard %}
|
||||
--DATA--
|
||||
return []
|
||||
--EXCEPTION--
|
||||
Twig\Error\SyntaxError: Unknown "foobar" function in "index.twig" at line 5.
|
||||
@@ -0,0 +1,60 @@
|
||||
--TEST--
|
||||
"guard" creates a compilation time condition on Twig callables availability
|
||||
--TEMPLATE--
|
||||
{% guard function constant %}
|
||||
{% guard filter foobar %}
|
||||
NEVER
|
||||
{{ 'a'|foobar }}
|
||||
{% else %}
|
||||
The constant function does exist, but the foobar filter does not
|
||||
{%- endguard %}
|
||||
{% else %}
|
||||
NEVER
|
||||
{% endguard %}
|
||||
|
||||
{% guard function constant -%}
|
||||
{% guard filter upper -%}
|
||||
The constant function does exist, and the upper filter as well
|
||||
{%- else %}
|
||||
NEVER
|
||||
{% endguard %}
|
||||
{% else %}
|
||||
NEVER
|
||||
{% endguard %}
|
||||
|
||||
{% guard filter foobar %}
|
||||
NEVER
|
||||
{{ 'a'|foobar }}
|
||||
{% else -%}
|
||||
{% guard function barfoo %}
|
||||
NEVER
|
||||
{% else -%}
|
||||
The foobar filter does not exist, and the barfoo function does not exist
|
||||
{%- endguard %}
|
||||
{% endguard %}
|
||||
|
||||
{% guard filter foobar %}
|
||||
NEVER
|
||||
{{ 'a'|foobar }}
|
||||
{% else %}
|
||||
{%- guard function constant -%}
|
||||
The foobar filter does not exist, but the constant function exists
|
||||
{% else -%}
|
||||
NEVER
|
||||
{% endguard %}
|
||||
{% endguard %}
|
||||
|
||||
{% guard function first %}
|
||||
{% guard function second %}
|
||||
NEVER
|
||||
{{ second() }}
|
||||
{% endguard %}
|
||||
{{ first() }}
|
||||
{% endguard %}
|
||||
--DATA--
|
||||
return []
|
||||
--EXPECT--
|
||||
The constant function does exist, but the foobar filter does not
|
||||
The constant function does exist, and the upper filter as well
|
||||
The foobar filter does not exist, and the barfoo function does not exist
|
||||
The foobar filter does not exist, but the constant function exists
|
||||
Reference in New Issue
Block a user