mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-31 04:27:00 +00:00
Make various optimization for dynamic Twig callables
This commit is contained in:
@@ -19,12 +19,13 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
|
||||
protected $options;
|
||||
|
||||
private $name;
|
||||
private $dynamicName;
|
||||
private $callable;
|
||||
private $arguments;
|
||||
|
||||
public function __construct(string $name, $callable = null, array $options = [])
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->name = $this->dynamicName = $name;
|
||||
$this->callable = $callable;
|
||||
$this->arguments = [];
|
||||
$this->options = array_merge([
|
||||
@@ -43,6 +44,11 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getDynamicName(): string
|
||||
{
|
||||
return $this->dynamicName;
|
||||
}
|
||||
|
||||
public function getCallable()
|
||||
{
|
||||
return $this->callable;
|
||||
@@ -68,8 +74,23 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
|
||||
return $this->options['needs_context'];
|
||||
}
|
||||
|
||||
public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->name = $name;
|
||||
$new->dynamicName = $dynamicName;
|
||||
$new->arguments = $arguments;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.12, use withDynamicArguments() instead
|
||||
*/
|
||||
public function setArguments(array $arguments): void
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.12', 'The "%s::setArguments()" method is deprecated, use "%s::withDynamicArguments()" instead.', static::class, static::class);
|
||||
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
|
||||
+28
-35
@@ -494,9 +494,9 @@ class ExpressionParser
|
||||
}
|
||||
|
||||
$args = $this->parseArguments(true);
|
||||
$class = $this->getFunctionNodeClass($name, $line);
|
||||
$function = $this->getFunction($name, $line);
|
||||
|
||||
return new $class($name, $args, $line);
|
||||
return new ($function->getNodeClass())($function->getName(), $args, $line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,9 +559,9 @@ class ExpressionParser
|
||||
$length = $this->parseExpression();
|
||||
}
|
||||
|
||||
$class = $this->getFilterNodeClass('slice', $token->getLine());
|
||||
$filter = $this->getFilter('slice', $token->getLine());
|
||||
$arguments = new Node([$arg, $length]);
|
||||
$filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine());
|
||||
$filter = new ($filter->getNodeClass())($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine());
|
||||
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ']');
|
||||
|
||||
@@ -586,16 +586,15 @@ class ExpressionParser
|
||||
while (true) {
|
||||
$token = $this->parser->getStream()->expect(Token::NAME_TYPE);
|
||||
|
||||
$name = new ConstantExpression($token->getValue(), $token->getLine());
|
||||
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
|
||||
$arguments = new Node();
|
||||
} else {
|
||||
$arguments = $this->parseArguments(true, false, true);
|
||||
}
|
||||
|
||||
$class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
|
||||
|
||||
$node = new $class($node, $name, $arguments, $token->getLine(), $tag);
|
||||
$filter = $this->getFilter($token->getValue(), $token->getLine());
|
||||
$name = new ConstantExpression($filter->getName(), $token->getLine());
|
||||
$node = new ($filter->getNodeClass())($node, $name, $arguments, $token->getLine(), $tag);
|
||||
|
||||
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) {
|
||||
break;
|
||||
@@ -724,9 +723,8 @@ class ExpressionParser
|
||||
private function parseTestExpression(Node $node): TestExpression
|
||||
{
|
||||
$stream = $this->parser->getStream();
|
||||
[$name, $test] = $this->getTest($node->getTemplateLine());
|
||||
$test = $this->getTest($node->getTemplateLine());
|
||||
|
||||
$class = $this->getTestNodeClass($test);
|
||||
$arguments = null;
|
||||
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
|
||||
$arguments = $this->parseArguments(true);
|
||||
@@ -734,42 +732,37 @@ class ExpressionParser
|
||||
$arguments = new Node([0 => $this->parsePrimaryExpression()]);
|
||||
}
|
||||
|
||||
if ('defined' === $name && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
|
||||
if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
|
||||
$node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
|
||||
$node->setAttribute('safe', true);
|
||||
}
|
||||
|
||||
return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine());
|
||||
return new ($test->getNodeClass())($node, $test->getName(), $arguments, $this->parser->getCurrentToken()->getLine());
|
||||
}
|
||||
|
||||
private function getTest(int $line): array
|
||||
private function getTest(int $line): TwigTest
|
||||
{
|
||||
$stream = $this->parser->getStream();
|
||||
$name = $stream->expect(Token::NAME_TYPE)->getValue();
|
||||
|
||||
if ($test = $this->env->getTest($name)) {
|
||||
return [$name, $test];
|
||||
}
|
||||
if (!$test = $this->env->getTest($name)) {
|
||||
if ($stream->test(/* Token::NAME_TYPE */ 5)) {
|
||||
// try 2-words tests
|
||||
$name = $name.' '.$this->parser->getCurrentToken()->getValue();
|
||||
|
||||
if ($stream->test(Token::NAME_TYPE)) {
|
||||
// try 2-words tests
|
||||
$name = $name.' '.$this->parser->getCurrentToken()->getValue();
|
||||
|
||||
if ($test = $this->env->getTest($name)) {
|
||||
$stream->next();
|
||||
|
||||
return [$name, $test];
|
||||
if ($test = $this->env->getTest($name)) {
|
||||
$stream->next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getTests()));
|
||||
if (!$test) {
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getTests()));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
private function getTestNodeClass(TwigTest $test): string
|
||||
{
|
||||
if ($test->isDeprecated()) {
|
||||
$stream = $this->parser->getStream();
|
||||
$message = \sprintf('Twig Test "%s" is deprecated', $test->getName());
|
||||
@@ -783,10 +776,10 @@ class ExpressionParser
|
||||
trigger_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message);
|
||||
}
|
||||
|
||||
return $test->getNodeClass();
|
||||
return $test;
|
||||
}
|
||||
|
||||
private function getFunctionNodeClass(string $name, int $line): string
|
||||
private function getFunction(string $name, int $line): TwigFunction
|
||||
{
|
||||
if (!$function = $this->env->getFunction($name)) {
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
|
||||
@@ -806,10 +799,10 @@ class ExpressionParser
|
||||
trigger_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message);
|
||||
}
|
||||
|
||||
return $function->getNodeClass();
|
||||
return $function;
|
||||
}
|
||||
|
||||
private function getFilterNodeClass(string $name, int $line): string
|
||||
private function getFilter(string $name, int $line): TwigFilter
|
||||
{
|
||||
if (!$filter = $this->env->getFilter($name)) {
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
|
||||
@@ -829,7 +822,7 @@ class ExpressionParser
|
||||
trigger_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message);
|
||||
}
|
||||
|
||||
return $filter->getNodeClass();
|
||||
return $filter;
|
||||
}
|
||||
|
||||
// checks that the node only contains "constant" elements
|
||||
|
||||
+31
-24
@@ -36,10 +36,16 @@ final class ExtensionSet
|
||||
private $visitors;
|
||||
/** @var array<string, TwigFilter> */
|
||||
private $filters;
|
||||
/** @var array<string, TwigFilter> */
|
||||
private $dynamicFilters;
|
||||
/** @var array<string, TwigTest> */
|
||||
private $tests;
|
||||
/** @var array<string, TwigTest> */
|
||||
private $dynamicTests;
|
||||
/** @var array<string, TwigFunction> */
|
||||
private $functions;
|
||||
/** @var array<string, TwigFunction> */
|
||||
private $dynamicFunctions;
|
||||
/** @var array<string, array{precedence: int, class: class-string<AbstractExpression>}> */
|
||||
private $unaryOperators;
|
||||
/** @var array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}> */
|
||||
@@ -167,14 +173,11 @@ final class ExtensionSet
|
||||
return $this->functions[$name];
|
||||
}
|
||||
|
||||
foreach ($this->functions as $pattern => $function) {
|
||||
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
|
||||
|
||||
if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
|
||||
foreach ($this->dynamicFunctions as $pattern => $function) {
|
||||
if (preg_match($pattern, $name, $matches)) {
|
||||
array_shift($matches);
|
||||
$function->setArguments($matches);
|
||||
|
||||
return $function;
|
||||
return $function->withDynamicArguments($name, $function->getName(), $matches);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,14 +226,11 @@ final class ExtensionSet
|
||||
return $this->filters[$name];
|
||||
}
|
||||
|
||||
foreach ($this->filters as $pattern => $filter) {
|
||||
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
|
||||
|
||||
if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
|
||||
foreach ($this->dynamicFilters as $pattern => $filter) {
|
||||
if (preg_match($pattern, $name, $matches)) {
|
||||
array_shift($matches);
|
||||
$filter->setArguments($matches);
|
||||
|
||||
return $filter;
|
||||
return $filter->withDynamicArguments($name, $filter->getName(), $matches);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,16 +375,11 @@ final class ExtensionSet
|
||||
return $this->tests[$name];
|
||||
}
|
||||
|
||||
foreach ($this->tests as $pattern => $test) {
|
||||
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
|
||||
foreach ($this->dynamicTests as $pattern => $test) {
|
||||
if (preg_match($pattern, $name, $matches)) {
|
||||
array_shift($matches);
|
||||
|
||||
if ($count) {
|
||||
if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
|
||||
array_shift($matches);
|
||||
$test->setArguments($matches);
|
||||
|
||||
return $test;
|
||||
}
|
||||
return $test->withDynamicArguments($name, $test->getName(), $matches);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +416,9 @@ final class ExtensionSet
|
||||
$this->filters = [];
|
||||
$this->functions = [];
|
||||
$this->tests = [];
|
||||
$this->dynamicFilters = [];
|
||||
$this->dynamicFunctions = [];
|
||||
$this->dynamicTests = [];
|
||||
$this->visitors = [];
|
||||
$this->unaryOperators = [];
|
||||
$this->binaryOperators = [];
|
||||
@@ -437,17 +435,26 @@ final class ExtensionSet
|
||||
{
|
||||
// filters
|
||||
foreach ($extension->getFilters() as $filter) {
|
||||
$this->filters[$filter->getName()] = $filter;
|
||||
$this->filters[$name = $filter->getName()] = $filter;
|
||||
if (str_contains($name, '*')) {
|
||||
$this->dynamicFilters['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $filter;
|
||||
}
|
||||
}
|
||||
|
||||
// functions
|
||||
foreach ($extension->getFunctions() as $function) {
|
||||
$this->functions[$function->getName()] = $function;
|
||||
$this->functions[$name = $function->getName()] = $function;
|
||||
if (str_contains($name, '*')) {
|
||||
$this->dynamicFunctions['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $function;
|
||||
}
|
||||
}
|
||||
|
||||
// tests
|
||||
foreach ($extension->getTests() as $test) {
|
||||
$this->tests[$test->getName()] = $test;
|
||||
$this->tests[$name = $test->getName()] = $test;
|
||||
if (str_contains($name, '*')) {
|
||||
$this->dynamicTests['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $test;
|
||||
}
|
||||
}
|
||||
|
||||
// token parsers
|
||||
|
||||
@@ -51,7 +51,7 @@ abstract class CallExpression extends AbstractExpression
|
||||
|
||||
$compiler->raw(\sprintf('->%s', $callable[1]));
|
||||
} else {
|
||||
$compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('name')));
|
||||
$compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('dynamic_name')));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ class FilterExpression extends CallExpression
|
||||
$this->setAttribute('arguments', $filter->getArguments());
|
||||
$this->setAttribute('callable', $filter->getCallable());
|
||||
$this->setAttribute('is_variadic', $filter->isVariadic());
|
||||
$this->setAttribute('dynamic_name', $filter->getDynamicName());
|
||||
|
||||
$this->compileCallable($compiler);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class FunctionExpression extends CallExpression
|
||||
}
|
||||
$this->setAttribute('callable', $callable);
|
||||
$this->setAttribute('is_variadic', $function->isVariadic());
|
||||
$this->setAttribute('dynamic_name', $function->getDynamicName());
|
||||
|
||||
$this->compileCallable($compiler);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class TestExpression extends CallExpression
|
||||
$this->setAttribute('arguments', $test->getArguments());
|
||||
$this->setAttribute('callable', $test->getCallable());
|
||||
$this->setAttribute('is_variadic', $test->isVariadic());
|
||||
$this->setAttribute('dynamic_name', $test->getDynamicName());
|
||||
|
||||
$this->compileCallable($compiler);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Twig\Tests;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\Binary\ConcatBinary;
|
||||
@@ -21,6 +22,9 @@ use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\NameExpression;
|
||||
use Twig\Parser;
|
||||
use Twig\Source;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
use Twig\TwigTest;
|
||||
|
||||
class ExpressionParserTest extends TestCase
|
||||
{
|
||||
@@ -411,6 +415,51 @@ class ExpressionParserTest extends TestCase
|
||||
$parser->parse($env->tokenize(new Source('{{ 1 is foobar }}', 'index')));
|
||||
}
|
||||
|
||||
public function testCompiledCodeForDynamicTest()
|
||||
{
|
||||
$env = new Environment(new ArrayLoader(['index' => '{{ "a" is foo_foo_bar_bar }}']), ['cache' => false, 'autoescape' => false]);
|
||||
$env->addExtension(new class() extends AbstractExtension {
|
||||
public function getTests()
|
||||
{
|
||||
return [
|
||||
new TwigTest('*_foo_*_bar', function ($foo, $bar, $a) {}),
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
$this->assertStringContainsString('$this->env->getTest(\'*_foo_*_bar\')->getCallable()("foo", "bar", "a")', $env->compile($env->parse($env->tokenize(new Source($env->getLoader()->getSourceContext('index')->getCode(), 'index')))));
|
||||
}
|
||||
|
||||
public function testCompiledCodeForDynamicFunction()
|
||||
{
|
||||
$env = new Environment(new ArrayLoader(['index' => '{{ foo_foo_bar_bar("a") }}']), ['cache' => false, 'autoescape' => false]);
|
||||
$env->addExtension(new class() extends AbstractExtension {
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new TwigFunction('*_foo_*_bar', function ($foo, $bar, $a) {}),
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
$this->assertStringContainsString('$this->env->getFunction(\'*_foo_*_bar\')->getCallable()("foo", "bar", "a")', $env->compile($env->parse($env->tokenize(new Source($env->getLoader()->getSourceContext('index')->getCode(), 'index')))));
|
||||
}
|
||||
|
||||
public function testCompiledCodeForDynamicFilter()
|
||||
{
|
||||
$env = new Environment(new ArrayLoader(['index' => '{{ "a"|foo_foo_bar_bar }}']), ['cache' => false, 'autoescape' => false]);
|
||||
$env->addExtension(new class() extends AbstractExtension {
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new TwigFilter('*_foo_*_bar', function ($foo, $bar, $a) {}),
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
$this->assertStringContainsString('$this->env->getFilter(\'*_foo_*_bar\')->getCallable()("foo", "bar", "a")', $env->compile($env->parse($env->tokenize(new Source($env->getLoader()->getSourceContext('index')->getCode(), 'index')))));
|
||||
}
|
||||
|
||||
private function createNameExpression(string $name, array $attributes)
|
||||
{
|
||||
$expression = new NameExpression($name, 1);
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
dynamic filter
|
||||
--TEMPLATE--
|
||||
{{ 'bar'|foo_path }}
|
||||
{{ 'bar'|bar_path }}
|
||||
{{ 'bar'|a_foo_b_bar }}
|
||||
--DATA--
|
||||
return []
|
||||
--EXPECT--
|
||||
foo/bar
|
||||
bar/bar
|
||||
a/b/bar
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
dynamic function
|
||||
--TEMPLATE--
|
||||
{{ foo_path('bar') }}
|
||||
{{ bar_path('bar') }}
|
||||
{{ a_foo_b_bar('bar') }}
|
||||
--DATA--
|
||||
return []
|
||||
--EXPECT--
|
||||
foo/bar
|
||||
bar/bar
|
||||
a/b/bar
|
||||
|
||||
Reference in New Issue
Block a user