Abstract node capture in its own Node

This commit is contained in:
Fabien Potencier
2023-12-27 16:11:54 +01:00
parent 6e2f2655a8
commit 6ebbc30593
8 changed files with 118 additions and 66 deletions
+9 -7
View File
@@ -12,13 +12,17 @@
namespace Twig\Extra\Cache\Node;
use Twig\Compiler;
use Twig\Node\CaptureNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Node;
class CacheNode extends Node
class CacheNode extends AbstractExpression
{
public function __construct(AbstractExpression $key, ?AbstractExpression $ttl, ?AbstractExpression $tags, Node $body, int $lineno, string $tag)
{
$body = new CaptureNode($body, $lineno);
$body->setAttribute('raw', true);
$nodes = ['key' => $key, 'body' => $body];
if (null !== $ttl) {
$nodes['ttl'] = $ttl;
@@ -34,7 +38,7 @@ class CacheNode extends Node
{
$compiler
->addDebugInfo($this)
->write('$cached = $this->env->getRuntime(\'Twig\Extra\Cache\CacheRuntime\')->getCache()->get(')
->raw('$this->env->getRuntime(\'Twig\Extra\Cache\CacheRuntime\')->getCache()->get(')
->subcompile($this->getNode('key'))
->raw(", function (\Symfony\Contracts\Cache\ItemInterface \$item) use (\$context, \$macros) {\n")
->indent()
@@ -57,13 +61,11 @@ class CacheNode extends Node
}
$compiler
->write("ob_start(function () { return ''; });\n")
->write('return ')
->subcompile($this->getNode('body'))
->write("\n")
->write("return ob_get_clean();\n")
->raw(";\n")
->outdent()
->write("});\n")
->write("echo '' === \$cached ? '' : new Markup(\$cached, \$this->env->getCharset());\n")
->write("})\n")
;
}
}
@@ -13,7 +13,10 @@ namespace Twig\Extra\Cache\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Extra\Cache\Node\CacheNode;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
@@ -54,7 +57,10 @@ class CacheTokenParser extends AbstractTokenParser
$body = $this->parser->subparse([$this, 'decideCacheEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
return new CacheNode($key, $ttl, $tags, $body, $token->getLine(), $this->getTag());
$body = new CacheNode($key, $ttl, $tags, $body, $token->getLine(), $this->getTag());
$body = new FilterExpression($body, new ConstantExpression('raw', $token->getLine()), new Node(), $token->getLine());
return new PrintNode($body, $token->getLine(), $this->getTag());
}
public function decideCacheEnd(Token $token): bool
+1 -1
View File
@@ -17,7 +17,7 @@
"require": {
"php": ">=7.2.5",
"symfony/cache": "^5.0|^6.0|^7.0",
"twig/twig": "^3.0"
"twig/twig": "^3.9"
},
"require-dev": {
"symfony/phpunit-bridge": "^6.4|^7.0"
+63
View File
@@ -0,0 +1,63 @@
<?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\Node;
use Twig\Compiler;
/**
* Represents a node for which we need to capture the output.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class CaptureNode extends Node
{
public function __construct(Node $body, int $lineno, string $tag = null)
{
parent::__construct(['body' => $body], ['raw' => false, 'with_blocks' => false], $lineno, $tag);
}
public function compile(Compiler $compiler): void
{
if ($this->getAttribute('with_blocks')) {
$compiler->raw("(function () use (&\$context, \$macros, \$blocks) {\n");
} else {
$compiler->raw("(function () use (&\$context, \$macros) {\n");
}
$compiler->indent();
if ($compiler->getEnvironment()->isDebug()) {
$compiler->write("ob_start();\n");
} else {
$compiler->write("ob_start(function () { return ''; });\n");
}
$compiler
->write("try {\n")
->indent()
->subcompile($this->getNode('body'))
->raw("\n")
;
if ($this->getAttribute('raw')) {
$compiler->write("return ob_get_contents();\n");
} else {
$compiler->write("return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());\n");
}
$compiler
->outdent()
->write("} finally {\n")
->indent()
->write("ob_end_clean();\n")
->outdent()
->write("}\n")
->outdent()
->write('})()')
;
}
}
+4 -20
View File
@@ -31,6 +31,8 @@ class MacroNode extends Node
}
}
$body = new CaptureNode($body, $lineno, $tag);
parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno, $tag);
}
@@ -81,31 +83,13 @@ class MacroNode extends Node
->write('')
->string(self::VARARGS_NAME)
->raw(' => ')
;
$compiler
->raw("\$__varargs__,\n")
->outdent()
->write("]);\n\n")
->write("\$blocks = [];\n\n")
;
if ($compiler->getEnvironment()->isDebug()) {
$compiler->write("ob_start();\n");
} else {
$compiler->write("ob_start(function () { return ''; });\n");
}
$compiler
->write("try {\n")
->indent()
->write('return ')
->subcompile($this->getNode('body'))
->raw("\n")
->write("return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());\n")
->outdent()
->write("} finally {\n")
->indent()
->write("ob_end_clean();\n")
->outdent()
->write("}\n")
->raw(";\n")
->outdent()
->write("}\n\n")
;
+14 -26
View File
@@ -23,22 +23,24 @@ class SetNode extends Node implements NodeCaptureInterface
{
public function __construct(bool $capture, Node $names, Node $values, int $lineno, string $tag = null)
{
parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => false], $lineno, $tag);
/*
* Optimizes the node when capture is used for a large block of text.
*
* {% set foo %}foo{% endset %} is compiled to $context['foo'] = new Twig\Markup("foo");
*/
if ($this->getAttribute('capture')) {
$this->setAttribute('safe', true);
$values = $this->getNode('values');
$safe = false;
if ($capture) {
$safe = true;
if ($values instanceof TextNode) {
$this->setNode('values', new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine()));
$this->setAttribute('capture', false);
$values = new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine());
$capture = false;
} else {
$values = new CaptureNode($values, $values->getTemplateLine());
$values->setAttribute('with_blocks', true);
}
}
parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => $safe], $lineno, $tag);
}
public function compile(Compiler $compiler): void
@@ -56,27 +58,13 @@ class SetNode extends Node implements NodeCaptureInterface
}
$compiler->raw(')');
} else {
if ($this->getAttribute('capture')) {
if ($compiler->getEnvironment()->isDebug()) {
$compiler->write("ob_start();\n");
} else {
$compiler->write("ob_start(function () { return ''; });\n");
}
$compiler
->subcompile($this->getNode('values'))
;
}
$compiler->subcompile($this->getNode('names'), false);
if ($this->getAttribute('capture')) {
$compiler->raw(" = ('' === \$tmp = ob_get_clean()) ? '' : new Markup(\$tmp, \$this->env->getCharset())");
}
}
$compiler->raw(' = ');
if (!$this->getAttribute('capture')) {
$compiler->raw(' = ');
if ($this->getAttribute('capture')) {
$compiler->subcompile($this->getNode('values'));
} else {
if (\count($this->getNode('names')) > 1) {
$compiler->write('[');
foreach ($this->getNode('values') as $idx => $value) {
+10 -8
View File
@@ -26,7 +26,7 @@ class MacroTest extends NodeTestCase
$arguments = new Node([new NameExpression('foo', 1)], [], 1);
$node = new MacroNode('foo', $body, $arguments, 1);
$this->assertEquals($body, $node->getNode('body'));
$this->assertEquals($body, $node->getNode('body')->getNode('body'));
$this->assertEquals($arguments, $node->getNode('arguments'));
$this->assertEquals('foo', $node->getAttribute('name'));
}
@@ -54,14 +54,16 @@ public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__)
\$blocks = [];
ob_start(function () { return ''; });
try {
echo "foo";
return (function () use (&\$context, \$macros) {
ob_start(function () { return ''; });
try {
echo "foo";
return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());
} finally {
ob_end_clean();
}
return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());
} finally {
ob_end_clean();
}
})();
}
EOF
],
+10 -3
View File
@@ -51,9 +51,16 @@ EOF
$node = new SetNode(true, $names, $values, 1);
$tests[] = [$node, <<<EOF
// line 1
ob_start(function () { return ''; });
echo "foo";
\$context["foo"] = ('' === \$tmp = ob_get_clean()) ? '' : new Markup(\$tmp, \$this->env->getCharset());
\$context["foo"] = (function () use (&\$context, \$macros, \$blocks) {
ob_start(function () { return ''; });
try {
echo "foo";
return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());
} finally {
ob_end_clean();
}
})();
EOF
];