mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-03 05:56:43 +00:00
Add "registerUndefinedTokenParserCallback"
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 3.2.0 (2021-XX-XX)
|
||||
|
||||
* n/a
|
||||
* Add "registerUndefinedTokenParserCallback"
|
||||
|
||||
# 3.1.1 (2020-10-27)
|
||||
|
||||
|
||||
+14
-8
@@ -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
|
||||
|
||||
+1
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user