Add "registerUndefinedTokenParserCallback"

This commit is contained in:
Fabien Potencier
2020-12-31 08:39:00 +01:00
parent dc7e21fb20
commit 81751d6671
9 changed files with 133 additions and 36 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# 3.2.0 (2021-XX-XX)
* n/a
* Add "registerUndefinedTokenParserCallback"
# 3.1.1 (2020-10-27)
+14 -8
View File
@@ -263,18 +263,24 @@ In the inner loop, the ``loop.parent`` variable is used to access the outer
context. So, the index of the current ``topic`` defined in the outer for loop
is accessible via the ``loop.parent.loop.index`` variable.
Defining undefined Functions and Filters on the Fly
---------------------------------------------------
Defining undefined Functions, Filters, and Tags on the Fly
----------------------------------------------------------
When a function (or a filter) is not defined, Twig defaults to throw a
``\Twig\Error\SyntaxError`` exception. However, it can also call a `callback`_ (any
valid PHP callable) which should return a function (or a filter).
.. versionadded:: 3.2
The ``registerUndefinedTokenParserCallback()`` method was added in Twig
3.2.
When a function/filter/tag is not defined, Twig defaults to throw a
``\Twig\Error\SyntaxError`` exception. However, it can also call a `callback`_
(any valid PHP callable) which should return a function/filter/tag.
For tags, register callbacks with ``registerUndefinedTokenParserCallback()``.
For filters, register callbacks with ``registerUndefinedFilterCallback()``.
For functions, use ``registerUndefinedFunctionCallback()``::
// auto-register all native PHP functions as Twig functions
// don't try this at home as it's not secure at all!
// NEVER do this in a project as it's NOT secure
$twig->registerUndefinedFunctionCallback(function ($name) {
if (function_exists($name)) {
return new \Twig\TwigFunction($name, $name);
@@ -283,7 +289,7 @@ For functions, use ``registerUndefinedFunctionCallback()``::
return false;
});
If the callable is not able to return a valid function (or filter), it must
If the callable is not able to return a valid function/filter/tag, it must
return ``false``.
If you register more than one callback, Twig will call them in turn until one
@@ -291,7 +297,7 @@ does not return ``false``.
.. tip::
As the resolution of functions and filters is done during compilation,
As the resolution of functions/filters/tags is done during compilation,
there is no overhead when registering these callbacks.
Validating the Template Syntax
@@ -23,6 +23,7 @@ class MissingExtensionSuggestorPass implements CompilerPassInterface
$container->getDefinition('twig')
->addMethodCall('registerUndefinedFilterCallback', [[new Reference('twig.missing_extension_suggestor'), 'suggestFilter']])
->addMethodCall('registerUndefinedFunctionCallback', [[new Reference('twig.missing_extension_suggestor'), 'suggestFunction']])
->addMethodCall('registerUndefinedTokenParserCallback', [[new Reference('twig.missing_extension_suggestor'), 'suggestTag']])
;
}
}
@@ -28,6 +28,7 @@ final class Extensions
'package' => 'twig/html-extra',
'filters' => ['data_uri'],
'functions' => ['html_classes'],
'tags' => [],
],
'markdown' => [
'name' => 'markdown',
@@ -36,6 +37,7 @@ final class Extensions
'package' => 'twig/markdown-extra',
'filters' => ['html_to_markdown', 'markdown_to_html'],
'functions' => [],
'tags' => [],
],
'intl' => [
'name' => 'intl',
@@ -48,6 +50,7 @@ final class Extensions
'format_duration_number', 'format_date', 'format_datetime', 'format_time',
],
'functions' => ['country_timezones'],
'tags' => [],
],
'cssinliner' => [
'name' => 'cssinliner',
@@ -56,6 +59,7 @@ final class Extensions
'package' => 'twig/cssinliner-extra',
'filters' => ['inline_css'],
'functions' => [],
'tags' => [],
],
'inky' => [
'name' => 'inky',
@@ -64,6 +68,7 @@ final class Extensions
'package' => 'twig/inky-extra',
'filters' => ['inky_to_html'],
'functions' => [],
'tags' => [],
],
'string' => [
'name' => 'string',
@@ -72,6 +77,7 @@ final class Extensions
'package' => 'twig/string-extra',
'filters' => ['u'],
'functions' => [],
'tags' => [],
],
];
@@ -101,4 +107,15 @@ final class Extensions
return [];
}
public static function getTag(string $name): array
{
foreach (self::EXTENSIONS as $extension) {
if (\in_array($name, $extension['tags'])) {
return [$extension['class_name'], $extension['package']];
}
}
return [];
}
}
@@ -32,4 +32,13 @@ final class MissingExtensionSuggestor
return false;
}
public function suggestTag(string $name): bool
{
if ($function = Extensions::getTag($name)) {
throw new SyntaxError(sprintf('The "%s" tag is part of the %s, which is not installed/enabled; try running "composer require %s".', $name, $function[0], $function[1]));
}
return false;
}
}
+9 -11
View File
@@ -554,7 +554,7 @@ class Environment
}
/**
* Returns the runtime implementation of a Twig element (filter/function/test).
* Returns the runtime implementation of a Twig element (filter/function/tag/test).
*
* @param string $class A runtime class name
*
@@ -616,18 +616,16 @@ class Environment
}
/**
* @return TokenParserInterface[]
*
* @internal
*/
public function getTags(): array
public function getTokenParser(string $name): ?TokenParserInterface
{
$tags = [];
foreach ($this->getTokenParsers() as $parser) {
$tags[$parser->getTag()] = $parser;
}
return $this->extensionSet->getTokenParser($name);
}
return $tags;
public function registerUndefinedTokenParserCallback(callable $callable): void
{
$this->extensionSet->registerUndefinedTokenParserCallback($callable);
}
public function addNodeVisitor(NodeVisitorInterface $visitor)
@@ -658,7 +656,7 @@ class Environment
return $this->extensionSet->getFilter($name);
}
public function registerUndefinedFilterCallback(callable $callable)
public function registerUndefinedFilterCallback(callable $callable): void
{
$this->extensionSet->registerUndefinedFilterCallback($callable);
}
@@ -715,7 +713,7 @@ class Environment
return $this->extensionSet->getFunction($name);
}
public function registerUndefinedFunctionCallback(callable $callable)
public function registerUndefinedFunctionCallback(callable $callable): void
{
$this->extensionSet->registerUndefinedFunctionCallback($callable);
}
+26 -1
View File
@@ -39,6 +39,7 @@ final class ExtensionSet
private $globals;
private $functionCallbacks = [];
private $filterCallbacks = [];
private $parserCallbacks = [];
private $lastModified = 0;
public function __construct()
@@ -280,6 +281,30 @@ final class ExtensionSet
return $this->parsers;
}
public function getTokenParser(string $name): ?TokenParserInterface
{
if (!$this->initialized) {
$this->initExtensions();
}
if (isset($this->parsers[$name])) {
return $this->parsers[$name];
}
foreach ($this->parserCallbacks as $callback) {
if (false !== $parser = $callback($name)) {
return $parser;
}
}
return null;
}
public function registerUndefinedTokenParserCallback(callable $callable): void
{
$this->parserCallbacks[] = $callable;
}
public function getGlobals(): array
{
if (null !== $this->globals) {
@@ -413,7 +438,7 @@ final class ExtensionSet
throw new \LogicException('getTokenParsers() must return an array of \Twig\TokenParser\TokenParserInterface.');
}
$this->parsers[] = $parser;
$this->parsers[$parser->getTag()] = $parser;
}
// node visitors
+3 -14
View File
@@ -34,7 +34,6 @@ class Parser
private $stack = [];
private $stream;
private $parent;
private $handlers;
private $visitors;
private $expressionParser;
private $blocks;
@@ -62,16 +61,6 @@ class Parser
unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames']);
$this->stack[] = $vars;
// tag handlers
if (null === $this->handlers) {
$this->handlers = [];
foreach ($this->env->getTokenParsers() as $handler) {
$handler->setParser($this);
$this->handlers[$handler->getTag()] = $handler;
}
}
// node visitors
if (null === $this->visitors) {
$this->visitors = $this->env->getNodeVisitors();
@@ -161,7 +150,7 @@ class Parser
return new Node($rv, [], $lineno);
}
if (!isset($this->handlers[$token->getValue()])) {
if (!$subparser = $this->env->getTokenParser($token->getValue())) {
if (null !== $test) {
$e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
@@ -170,7 +159,7 @@ class Parser
}
} else {
$e = new SyntaxError(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
$e->addSuggestions($token->getValue(), array_keys($this->env->getTags()));
$e->addSuggestions($token->getValue(), array_keys($this->env->getTokenParsers()));
}
throw $e;
@@ -178,7 +167,7 @@ class Parser
$this->stream->next();
$subparser = $this->handlers[$token->getValue()];
$subparser->setParser($this);
$node = $subparser->parse($token);
if (null !== $node) {
$rv[] = $node;
+53 -1
View File
@@ -27,6 +27,7 @@ use Twig\RuntimeLoader\RuntimeLoaderInterface;
use Twig\Source;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
@@ -275,7 +276,7 @@ class EnvironmentTest extends TestCase
$twig = new Environment($this->createMock(LoaderInterface::class));
$twig->addExtension(new EnvironmentTest_Extension());
$this->assertArrayHasKey('test', $twig->getTags());
$this->assertArrayHasKey('test', $twig->getTokenParsers());
$this->assertArrayHasKey('foo_filter', $twig->getFilters());
$this->assertArrayHasKey('foo_function', $twig->getFunctions());
$this->assertArrayHasKey('foo_test', $twig->getTests());
@@ -351,6 +352,57 @@ class EnvironmentTest extends TestCase
$twig->loadTemplate($twig->getTemplateClass($template), $template, 112233);
}
public function testUndefinedFunctionCallback()
{
$twig = new Environment($this->createMock(LoaderInterface::class));
$twig->registerUndefinedFunctionCallback(function (string $name) {
if ('dynamic' === $name) {
return new TwigFunction('dynamic', function () { return 'dynamic'; });
}
return false;
});
$this->assertNull($twig->getFunction('does_not_exist'));
$this->assertInstanceOf(TwigFunction::class, $function = $twig->getFunction('dynamic'));
$this->assertSame('dynamic', $function->getName());
}
public function testUndefinedFilterCallback()
{
$twig = new Environment($this->createMock(LoaderInterface::class));
$twig->registerUndefinedFilterCallback(function (string $name) {
if ('dynamic' === $name) {
return new TwigFilter('dynamic', function () { return 'dynamic'; });
}
return false;
});
$this->assertNull($twig->getFilter('does_not_exist'));
$this->assertInstanceOf(TwigFilter::class, $filter = $twig->getFilter('dynamic'));
$this->assertSame('dynamic', $filter->getName());
}
public function testUndefinedTokenParserCallback()
{
$twig = new Environment($this->createMock(LoaderInterface::class));
$twig->registerUndefinedTokenParserCallback(function (string $name) {
if ('dynamic' === $name) {
$parser = $this->createMock(TokenParserInterface::class);
$parser->expects($this->once())->method('getTag')->willReturn('dynamic');
return $parser;
}
return false;
});
$this->assertNull($twig->getTokenParser('does_not_exist'));
$this->assertInstanceOf(TokenParserInterface::class, $parser = $twig->getTokenParser('dynamic'));
$this->assertSame('dynamic', $parser->getTag());
}
protected function getMockLoader($templateName, $templateContent)
{
$loader = $this->createMock(LoaderInterface::class);