Make various optimization for dynamic Twig callables

This commit is contained in:
Fabien Potencier
2024-08-11 14:21:57 +02:00
parent 2fa8a24e0b
commit 148d3e079d
10 changed files with 138 additions and 61 deletions
+22 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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')));
}
}
+1
View File
@@ -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);
}
+1
View File
@@ -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);
}
+49
View File
@@ -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