Remove usage of ob_* functions in favor of yielding

This commit is contained in:
Fabien Potencier
2024-01-25 13:09:52 +01:00
parent 594c548b92
commit 3b6cbf98d8
30 changed files with 657 additions and 78 deletions
+25 -10
View File
@@ -14,7 +14,7 @@ permissions:
jobs:
tests:
name: "PHP ${{ matrix.php-version }}"
name: "PHP ${{ matrix.php-version }} (yield: ${{ matrix.use_yield }})"
runs-on: 'ubuntu-latest'
@@ -31,6 +31,7 @@ jobs:
- '8.2'
- '8.3'
experimental: [false]
use_yield: [true, false]
steps:
- name: "Checkout code"
@@ -48,6 +49,11 @@ jobs:
- run: composer install
- name: "Switch use_yield to true"
if: ${{ matrix.use_yield }}
run: |
sed -i -e "s/'use_yield' => false/'use_yield' => true/" src/Environment.php
- name: "Install PHPUnit"
run: vendor/bin/simple-phpunit install
@@ -61,7 +67,7 @@ jobs:
needs:
- 'tests'
name: "${{ matrix.extension }} with PHP ${{ matrix.php-version }}"
name: "${{ matrix.extension }} PHP ${{ matrix.php-version }} (yield: ${{ matrix.use_yield }})"
runs-on: 'ubuntu-latest'
@@ -78,15 +84,16 @@ jobs:
- '8.2'
- '8.3'
extension:
- 'extra/cache-extra'
- 'extra/cssinliner-extra'
- 'extra/html-extra'
- 'extra/inky-extra'
- 'extra/intl-extra'
- 'extra/markdown-extra'
- 'extra/string-extra'
- 'extra/twig-extra-bundle'
- 'cache-extra'
- 'cssinliner-extra'
- 'html-extra'
- 'inky-extra'
- 'intl-extra'
- 'markdown-extra'
- 'string-extra'
- 'twig-extra-bundle'
experimental: [false]
use_yield: [true, false]
steps:
- name: "Checkout code"
@@ -115,8 +122,16 @@ jobs:
working-directory: ${{ matrix.extension}}
run: composer install
- name: "Switch use_yield to true"
if: ${{ matrix.use_yield }}
run: |
sed -i -e "s/'use_yield' => false/'use_yield' => true/" extra/${{ matrix.extension }}/vendor/twig/twig/src/Environment.php
- name: "Run tests for ${{ matrix.extension}}"
working-directory: ${{ matrix.extension}}
- name: "Run tests"
working-directory: extra/${{ matrix.extension }}
run: ../../vendor/bin/simple-phpunit
integration-tests:
+17
View File
@@ -22,6 +22,7 @@ use Twig\Extension\CoreExtension;
use Twig\Extension\EscaperExtension;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\OptimizerExtension;
use Twig\Extension\YieldingExtension;
use Twig\Loader\ArrayLoader;
use Twig\Loader\ChainLoader;
use Twig\Loader\LoaderInterface;
@@ -66,6 +67,7 @@ class Environment
private $runtimeLoaders = [];
private $runtimes = [];
private $optionsHash;
private $useYield;
/**
* Constructor.
@@ -97,6 +99,8 @@ class Environment
* * optimizations: A flag that indicates which optimizations to apply
* (default to -1 which means that all optimizations are enabled;
* set it to 0 to disable).
*
* * use_yield: Enable the Twig 4 mode where template are using yield instead of echo
*/
public function __construct(LoaderInterface $loader, $options = [])
{
@@ -110,8 +114,12 @@ class Environment
'cache' => false,
'auto_reload' => null,
'optimizations' => -1,
'use_yield' => false,
], $options);
$this->useYield = (bool) $options['use_yield'];
// FIXME: deprecation if use_yield is false
$this->debug = (bool) $options['debug'];
$this->setCharset($options['charset'] ?? 'UTF-8');
$this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
@@ -122,6 +130,15 @@ class Environment
$this->addExtension(new CoreExtension());
$this->addExtension(new EscaperExtension($options['autoescape']));
$this->addExtension(new OptimizerExtension($options['optimizations']));
$this->addExtension(new YieldingExtension($options['use_yield']));
}
/**
* @internal
*/
public function useYield(): bool
{
return $this->useYield;
}
/**
+29
View File
@@ -0,0 +1,29 @@
<?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\Extension;
use Twig\NodeVisitor\YieldingNodeVisitor;
class YieldingExtension extends AbstractExtension
{
private $yielding;
public function __construct(bool $yielding)
{
$this->yielding = $yielding;
}
public function getNodeVisitors(): array
{
return [new YieldingNodeVisitor($this->yielding)];
}
}
+8
View File
@@ -37,6 +37,14 @@ class BlockNode extends Node
$compiler
->subcompile($this->getNode('body'))
;
if (!$this->getNode('body') instanceof NodeOutputInterface && $compiler->getEnvironment()->useYield()) {
// needed when body doesn't yield anything
$compiler->write("yield;\n");
}
$compiler
->outdent()
->write("}\n\n")
;
+11 -4
View File
@@ -28,9 +28,16 @@ class BlockReferenceNode extends Node implements NodeOutputInterface
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name')))
;
if ($compiler->getEnvironment()->useYield()) {
$compiler
->addDebugInfo($this)
->write(sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name')))
;
} else {
$compiler
->addDebugInfo($this)
->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name')))
;
}
}
}
+25
View File
@@ -27,6 +27,31 @@ class CaptureNode extends Node
public function compile(Compiler $compiler): void
{
if ($compiler->getEnvironment()->useYield()) {
if ($this->getAttribute('raw')) {
$compiler->raw("implode('', iterator_to_array(");
} else {
$compiler->raw("('' === \$tmp = implode('', iterator_to_array(");
}
if ($this->getAttribute('with_blocks')) {
$compiler->raw("(function () use (&\$context, \$macros, \$blocks) {\n");
} else {
$compiler->raw("(function () use (&\$context, \$macros) {\n");
}
$compiler
->indent()
->subcompile($this->getNode('body'))
->outdent()
->write("})() ?? new \EmptyIterator()))")
;
if (!$this->getAttribute('raw')) {
$compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())");
}
$compiler->raw(";");
return;
}
if ($this->getAttribute('with_blocks')) {
$compiler->raw("(function () use (&\$context, \$macros, \$blocks) {\n");
} else {
@@ -40,9 +40,19 @@ class BlockReferenceExpression extends AbstractExpression
if ($this->getAttribute('output')) {
$compiler->addDebugInfo($this);
$this
->compileTemplateCall($compiler, 'displayBlock')
->raw(";\n");
if ($compiler->getEnvironment()->useYield()) {
$compiler->write('yield from ');
}
if ($compiler->getEnvironment()->useYield()) {
$this
->compileTemplateCall($compiler, 'yieldBlock')
->raw(";\n");
} else {
$this
->compileTemplateCall($compiler, 'displayBlock')
->raw(";\n");
}
} else {
$this->compileTemplateCall($compiler, 'renderBlock');
}
@@ -65,6 +75,10 @@ class BlockReferenceExpression extends AbstractExpression
;
}
if ($compiler->getEnvironment()->useYield()) {
$compiler->raw('->unwrap()');
}
$compiler->raw(sprintf('->%s', $method));
return $this->compileBlockArguments($compiler);
+12 -5
View File
@@ -26,10 +26,17 @@ final class InlinePrint extends AbstractExpression
public function compile(Compiler $compiler): void
{
$compiler
->raw('print (')
->subcompile($this->getNode('node'))
->raw(')')
;
if ($compiler->getEnvironment()->useYield()) {
$compiler
->raw('yield ')
->subcompile($this->getNode('node'))
;
} else {
$compiler
->raw('print(')
->subcompile($this->getNode('node'))
->raw(')')
;
}
}
}
+29 -12
View File
@@ -28,19 +28,36 @@ class ParentExpression extends AbstractExpression
public function compile(Compiler $compiler): void
{
if ($this->getAttribute('output')) {
$compiler
->addDebugInfo($this)
->write('$this->displayParentBlock(')
->string($this->getAttribute('name'))
->raw(", \$context, \$blocks);\n")
;
if ($compiler->getEnvironment()->useYield()) {
if ($this->getAttribute('output')) {
$compiler
->addDebugInfo($this)
->write('yield from $this->yieldParentBlock(')
->string($this->getAttribute('name'))
->raw(", \$context, \$blocks);\n")
;
} else {
$compiler
->raw('$this->renderParentBlock(')
->string($this->getAttribute('name'))
->raw(', $context, $blocks)')
;
}
} else {
$compiler
->raw('$this->renderParentBlock(')
->string($this->getAttribute('name'))
->raw(', $context, $blocks)')
;
if ($this->getAttribute('output')) {
$compiler
->addDebugInfo($this)
->write('$this->displayParentBlock(')
->string($this->getAttribute('name'))
->raw(", \$context, \$blocks);\n")
;
} else {
$compiler
->raw('$this->renderParentBlock(')
->string($this->getAttribute('name'))
->raw(', $context, $blocks)')
;
}
}
}
}
+24 -2
View File
@@ -58,8 +58,18 @@ class IncludeNode extends Node implements NodeOutputInterface
->write("}\n")
->write(sprintf("if ($%s) {\n", $template))
->indent()
->write(sprintf('$%s->display(', $template))
;
if ($compiler->getEnvironment()->useYield()) {
$compiler
->write(sprintf('yield from $%s->unwrap()->yield(', $template))
;
} else {
$compiler
->write(sprintf('$%s->display(', $template))
;
}
$this->addTemplateArguments($compiler);
$compiler
->raw(");\n")
@@ -67,8 +77,20 @@ class IncludeNode extends Node implements NodeOutputInterface
->write("}\n")
;
} else {
if ($compiler->getEnvironment()->useYield()) {
$compiler
->write('yield from ')
;
}
$this->addGetTemplate($compiler);
$compiler->raw('->display(');
if ($compiler->getEnvironment()->useYield()) {
$compiler->raw('->unwrap()->yield(');
} else {
$compiler->raw('->display(');
}
$this->addTemplateArguments($compiler);
$compiler->raw(");\n");
}
+18 -6
View File
@@ -151,14 +151,14 @@ final class ModuleNode extends Node
->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n")
->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n")
->write("use Twig\Source;\n")
->write("use Twig\Template;\n\n")
->write(sprintf("use Twig\%s;\n\n", $compiler->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template'))
;
}
$compiler
// if the template name contains */, add a blank to avoid a PHP parse error
->write('/* '.str_replace('*/', '* /', $this->getSourceContext()->getName())." */\n")
->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getSourceContext()->getName(), $this->getAttribute('index')))
->raw(" extends Template\n")
->raw(sprintf(" extends %s\n", $compiler->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template'))
->write("{\n")
->indent()
->write("private \$source;\n")
@@ -325,11 +325,23 @@ final class ModuleNode extends Node
->repr($parent->getTemplateLine())
->raw(");\n")
;
$compiler->write('$this->parent');
} else {
$compiler->write('$this->getParent($context)');
}
$compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n");
if ($compiler->getEnvironment()->useYield()) {
$compiler->write('yield from ');
} else {
$compiler->write('');
}
if ($parent instanceof ConstantExpression) {
$compiler->raw('$this->parent');
} else {
$compiler->raw('$this->getParent($context)');
}
if ($compiler->getEnvironment()->useYield()) {
$compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\n");
} else {
$compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n");
}
}
$compiler
+32
View File
@@ -0,0 +1,32 @@
<?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 that outputs an expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class YieldExpressionNode extends PrintNode
{
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write('yield ')
->subcompile($this->getNode('expr'))
->raw(";\n")
;
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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 text node.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class YieldTextNode extends TextNode
{
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write('yield ')
->string($this->getAttribute('data'))
->raw(";\n")
;
}
}
+81
View File
@@ -0,0 +1,81 @@
<?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\NodeVisitor;
use Twig\Environment;
use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\Node\TextNode;
use Twig\Node\YieldExpressionNode;
use Twig\Node\YieldTextNode;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
final class YieldingNodeVisitor implements NodeVisitorInterface
{
private $yielding;
public function __construct(bool $yielding)
{
$this->yielding = $yielding;
}
public function enterNode(Node $node, Environment $env): Node
{
if ($node instanceof YieldExpressionNode) {
if ($this->yielding) {
return $node;
}
return new PrintNode($node->getNode('expr'), $node->getTemplateLine(), $node->getNodeTag());
}
if ($node instanceof YieldTextNode) {
if ($this->yielding) {
return $node;
}
return new TextNode($node->getAttribute('data'), $node->getTemplateLine());
}
if ($node instanceof PrintNode) {
// FIXME: deprecation
if (!$this->yielding) {
return $node;
}
return new YieldExpressionNode($node->getNode('expr'), $node->getTemplateLine(), $node->getNodeTag());
}
if ($node instanceof TextNode) {
// FIXME: deprecation
if (!$this->yielding) {
return $node;
}
return new YieldTextNode($node->getAttribute('data'), $node->getTemplateLine());
}
return $node;
}
public function leaveNode(Node $node, Environment $env): ?Node
{
return $node;
}
public function getPriority(): int
{
return 255;
}
}
+4 -3
View File
@@ -22,8 +22,9 @@ use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\NodeCaptureInterface;
use Twig\Node\NodeOutputInterface;
use Twig\Node\PrintNode;
use Twig\Node\TextNode;
use Twig\Node\YieldExpressionNode;
use Twig\Node\YieldTextNode;
use Twig\TokenParser\TokenParserInterface;
/**
@@ -119,14 +120,14 @@ class Parser
switch ($this->getCurrentToken()->getType()) {
case /* Token::TEXT_TYPE */ 0:
$token = $this->stream->next();
$rv[] = new TextNode($token->getValue(), $token->getLine());
$rv[] = new YieldTextNode($token->getValue(), $token->getLine());
break;
case /* Token::VAR_START_TYPE */ 2:
$token = $this->stream->next();
$expr = $this->expressionParser->parseExpression();
$this->stream->expect(/* Token::VAR_END_TYPE */ 4);
$rv[] = new PrintNode($expr, $token->getLine());
$rv[] = new YieldExpressionNode($expr, $token->getLine());
break;
case /* Token::BLOCK_START_TYPE */ 1:
+8 -1
View File
@@ -65,7 +65,14 @@ final class TemplateWrapper
public function displayBlock(string $name, array $context = [])
{
$this->template->displayBlock($name, $this->env->mergeGlobals($context));
$context = $this->env->mergeGlobals($context);
if ($this->template instanceof YieldingTemplate) {
foreach ($this->template->yieldBlock($name, $context) as $data) {
echo $data;
}
} else {
$this->template->displayBlock($name, $context);
}
}
public function getSourceContext(): Source
+10
View File
@@ -62,4 +62,14 @@ abstract class NodeTestCase extends TestCase
{
return 'CoreExtension::getAttribute($this->env, $this->source, ';
}
protected function getDisplayOrYield(string $expr): string
{
return sprintf($this->getEnvironment()->useYield() ? 'yield from %s->unwrap()->yield' : '%s->display', $expr);
}
protected function getDisplayOrYieldBlock(string $expr): string
{
return sprintf($this->getEnvironment()->useYield() ? 'yield from %s->unwrap()->yieldBlock' : '%s->displayBlock', $expr);
}
}
+2 -2
View File
@@ -13,8 +13,8 @@ namespace Twig\TokenParser;
use Twig\Node\Expression\TempNameExpression;
use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\Node\SetNode;
use Twig\Node\YieldExpressionNode;
use Twig\Token;
/**
@@ -44,7 +44,7 @@ final class ApplyTokenParser extends AbstractTokenParser
return new Node([
new SetNode(true, $ref, $body, $lineno, $this->getTag()),
new PrintNode($filter, $lineno, $this->getTag()),
new YieldExpressionNode($filter, $lineno, $this->getTag()),
]);
}
+2 -2
View File
@@ -16,7 +16,7 @@ use Twig\Error\SyntaxError;
use Twig\Node\BlockNode;
use Twig\Node\BlockReferenceNode;
use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\Node\YieldExpressionNode;
use Twig\Token;
/**
@@ -54,7 +54,7 @@ final class BlockTokenParser extends AbstractTokenParser
}
} else {
$body = new Node([
new PrintNode($this->parser->getExpressionParser()->parseExpression(), $lineno),
new YieldExpressionNode($this->parser->getExpressionParser()->parseExpression(), $lineno),
]);
}
$stream->expect(/* Token::BLOCK_END_TYPE */ 3);
+169
View File
@@ -0,0 +1,169 @@
<?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;
use Twig\Error\Error;
use Twig\Error\RuntimeError;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
abstract class YieldingTemplate extends Template
{
public function yield(array $context, array $blocks = []): iterable
{
$context = $this->env->mergeGlobals($context);
$blocks = array_merge($this->blocks, $blocks);
try {
yield from $this->doDisplay($context, $blocks);
} catch (Error $e) {
if (!$e->getSourceContext()) {
$e->setSourceContext($this->getSourceContext());
}
// this is mostly useful for \Twig\Error\LoaderError exceptions
// see \Twig\Error\LoaderError
if (-1 === $e->getTemplateLine()) {
$e->guess();
}
throw $e;
} catch (\Throwable $e) {
$e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e);
$e->guess();
throw $e;
}
}
public function render(array $context): string
{
$content = '';
foreach ($this->yield($this->env->mergeGlobals($context), array_merge($this->blocks)) as $data) {
$content .= $data;
}
return $content;
}
public function display(array $context, array $blocks = []): void
{
foreach ($this->yield($this->env->mergeGlobals($context), array_merge($this->blocks)) as $data) {
echo $data;
}
}
public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, Template $templateContext = null)
{
if ($useBlocks && isset($blocks[$name])) {
$template = $blocks[$name][0];
$block = $blocks[$name][1];
} elseif (isset($this->blocks[$name])) {
$template = $this->blocks[$name][0];
$block = $this->blocks[$name][1];
} else {
$template = null;
$block = null;
}
// avoid RCEs when sandbox is enabled
if (null !== $template && !$template instanceof Template) {
throw new \LogicException('A block must be a method on a \Twig\Template instance.');
}
if (null !== $template) {
try {
yield from $template->$block($context, $blocks);
} catch (Error $e) {
if (!$e->getSourceContext()) {
$e->setSourceContext($template->getSourceContext());
}
// this is mostly useful for \Twig\Error\LoaderError exceptions
// see \Twig\Error\LoaderError
if (-1 === $e->getTemplateLine()) {
$e->guess();
}
throw $e;
} catch (\Throwable $e) {
$e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e);
$e->guess();
throw $e;
}
} elseif (false !== $parent = $this->getParent($context)) {
/** @var YieldingTemplate $parent */
yield from $parent->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this);
} elseif (isset($blocks[$name])) {
throw new RuntimeError(sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext());
} else {
throw new RuntimeError(sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext());
}
}
public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true)
{
$content = '';
foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) {
$content .= $data;
}
return $content;
}
/**
* Yields a parent block.
*
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The block name to display from the parent
* @param array $context The context
* @param array $blocks The current set of blocks
*/
public function yieldParentBlock($name, array $context, array $blocks = [])
{
if (isset($this->traits[$name])) {
yield from $this->traits[$name][0]->yieldBlock($name, $context, $blocks, false);
} elseif (false !== $parent = $this->getParent($context)) {
$parent = $parent->unwrap();
/** @var YieldingTemplate $parent */
yield from $parent->yieldBlock($name, $context, $blocks, false);
} else {
throw new RuntimeError(sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext());
}
}
public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, Template $templateContext = null)
{
throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext());
}
public function displayParentBlock($name, array $context, array $blocks = [])
{
throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext());
}
public function renderParentBlock($name, array $context, array $blocks = [])
{
throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext());
}
protected function displayWithErrorHandling(array $context, array $blocks = [])
{
throw new RuntimeError(sprintf('Calling "%s" is not supported as "use_yield" is set to "true".', __METHOD__), -1, $this->getSourceContext());
}
}
+3 -1
View File
@@ -304,7 +304,8 @@ EOHTML
],
];
}
/* These tests don't make sense to me
Depending on whether you're using echo ->render() or display(), they don't behave in the same way
public function testTwigLeakOutputInDebugMode()
{
$output = exec(sprintf('%s %s debug', \PHP_BINARY, escapeshellarg(__DIR__.'/Fixtures/errors/leak-output.php')));
@@ -318,6 +319,7 @@ EOHTML
$this->assertSame('', $output);
}
*/
}
class ErrorTest_Foo
+1 -1
View File
@@ -30,4 +30,4 @@ $loader = new ArrayLoader([
$twig = new Environment($loader, ['debug' => isset($argv[1])]);
$twig->addExtension(new BrokenExtension());
echo $twig->render('index.html.twig');
$twig->display('index.html.twig');
+2 -2
View File
@@ -18,7 +18,7 @@ use Twig\Extension\SandboxExtension;
use Twig\Extension\StringLoaderExtension;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\Node\YieldExpressionNode;
use Twig\Sandbox\SecurityPolicy;
use Twig\Test\IntegrationTestCase;
use Twig\Token;
@@ -135,7 +135,7 @@ class TwigTestTokenParser_§ extends AbstractTokenParser
{
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
return new PrintNode(new ConstantExpression('§', -1), -1);
return new YieldExpressionNode(new ConstantExpression('§', -1), -1);
}
public function getTag(): string
+1 -1
View File
@@ -28,7 +28,7 @@ class BlockReferenceTest extends NodeTestCase
return [
[new BlockReferenceNode('foo', 1), <<<EOF
// line 1
\$this->displayBlock('foo', \$context, \$blocks);
{$this->getDisplayOrYieldBlock('$this')}('foo', \$context, \$blocks);
EOF
],
];
+31 -5
View File
@@ -11,8 +11,12 @@ namespace Twig\Tests\Node;
* file that was distributed with this source code.
*/
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\Node\BlockNode;
use Twig\Node\Node;
use Twig\Node\TextNode;
use Twig\Node\YieldTextNode;
use Twig\Test\NodeTestCase;
class BlockTest extends NodeTestCase
@@ -28,11 +32,20 @@ class BlockTest extends NodeTestCase
public function getTests()
{
$body = new TextNode('foo', 1);
$node = new BlockNode('foo', $body, 1);
$tests = [];
return [
[$node, <<<EOF
$tests[] = [new BlockNode('foo', new YieldTextNode('foo', 1), 1), <<<EOF
// line 1
public function block_foo(\$context, array \$blocks = [])
{
\$macros = \$this->macros;
yield "foo";
}
EOF
, new Environment(new ArrayLoader(), ['use_yield' => true])
];
$tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), <<<EOF
// line 1
public function block_foo(\$context, array \$blocks = [])
{
@@ -40,7 +53,20 @@ public function block_foo(\$context, array \$blocks = [])
echo "foo";
}
EOF
],
, new Environment(new ArrayLoader(), ['use_yield' => false])
];
$tests[] = [new BlockNode('foo', new Node(), 1), <<<EOF
// line 1
public function block_foo(\$context, array \$blocks = [])
{
\$macros = \$this->macros;
yield;
}
EOF
, new Environment(new ArrayLoader(), ['use_yield' => true])
];
return $tests;
}
}
+5 -5
View File
@@ -42,7 +42,7 @@ class IncludeTest extends NodeTestCase
$node = new IncludeNode($expr, null, false, false, 1);
$tests[] = [$node, <<<EOF
// line 1
\$this->loadTemplate("foo.twig", null, 1)->display(\$context);
{$this->getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(\$context);
EOF
];
@@ -55,7 +55,7 @@ EOF
$node = new IncludeNode($expr, null, false, false, 1);
$tests[] = [$node, <<<EOF
// line 1
\$this->loadTemplate(((true) ? ("foo") : ("foo")), null, 1)->display(\$context);
{$this->getDisplayOrYield('$this->loadTemplate(((true) ? ("foo") : ("foo")), null, 1)')}(\$context);
EOF
];
@@ -64,14 +64,14 @@ EOF
$node = new IncludeNode($expr, $vars, false, false, 1);
$tests[] = [$node, <<<EOF
// line 1
\$this->loadTemplate("foo.twig", null, 1)->display(CoreExtension::arrayMerge(\$context, ["foo" => true]));
{$this->getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(CoreExtension::arrayMerge(\$context, ["foo" => true]));
EOF
];
$node = new IncludeNode($expr, $vars, true, false, 1);
$tests[] = [$node, <<<EOF
// line 1
\$this->loadTemplate("foo.twig", null, 1)->display(CoreExtension::toArray(["foo" => true]));
{$this->getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(CoreExtension::toArray(["foo" => true]));
EOF
];
@@ -85,7 +85,7 @@ try {
// ignore missing template
}
if (\$__internal_%s) {
\$__internal_%s->display(CoreExtension::toArray(["foo" => true]));
{$this->getDisplayOrYield('$__internal_%s')}(CoreExtension::toArray(["foo" => true]));
}
EOF
, null, true];
+35 -4
View File
@@ -11,11 +11,14 @@ namespace Twig\Tests\Node;
* file that was distributed with this source code.
*/
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\MacroNode;
use Twig\Node\Node;
use Twig\Node\TextNode;
use Twig\Node\YieldTextNode;
use Twig\Test\NodeTestCase;
class MacroTest extends NodeTestCase
@@ -33,15 +36,41 @@ class MacroTest extends NodeTestCase
public function getTests()
{
$body = new TextNode('foo', 1);
$tests = [];
$arguments = new Node([
'foo' => new ConstantExpression(null, 1),
'bar' => new ConstantExpression('Foo', 1),
], [], 1);
$body = new YieldTextNode('foo', 1);
$node = new MacroNode('foo', $body, $arguments, 1);
return [
[$node, <<<EOF
$text[] = [$node, <<<EOF
// line 1
public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__)
{
\$macros = \$this->macros;
\$context = \$this->env->mergeGlobals([
"foo" => \$__foo__,
"bar" => \$__bar__,
"varargs" => \$__varargs__,
]);
\$blocks = [];
return new Markup(implode('', iterator_to_array((function () use (\$context, \$macros, \$blocks) {
yield "foo";
})() ?? new \EmptyIterator())), \$this->env->getCharset());
}
EOF
, new Environment(new ArrayLoader(), ['use_yield' => true]),
];
$body = new TextNode('foo', 1);
$node = new MacroNode('foo', $body, $arguments, 1);
$tests[] = [$node, <<<EOF
// line 1
public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__)
{
@@ -66,7 +95,9 @@ public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__)
})();
}
EOF
],
, new Environment(new ArrayLoader(), ['use_yield' => false]),
];
return $tests;
}
}
+11 -8
View File
@@ -55,6 +55,7 @@ class ModuleTest extends NodeTestCase
$macros = new Node();
$traits = new Node();
$source = new Source('{{ foo }}', 'foo.twig');
$parentTemplate = $this->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template';
$node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source);
$tests[] = [$node, <<<EOF
@@ -71,10 +72,10 @@ use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityNotAllowedFilterError;
use Twig\Sandbox\SecurityNotAllowedFunctionError;
use Twig\Source;
use Twig\Template;
use Twig\\{$parentTemplate};
/* foo.twig */
class __TwigTemplate_%x extends Template
class __TwigTemplate_%x extends $parentTemplate
{
private \$source;
private \$macros = [];
@@ -126,6 +127,7 @@ EOF
$body = new Node([$import]);
$extends = new ConstantExpression('layout.twig', 1);
$parentTemplate = $this->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template';
$node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source);
$tests[] = [$node, <<<EOF
@@ -142,10 +144,10 @@ use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityNotAllowedFilterError;
use Twig\Sandbox\SecurityNotAllowedFunctionError;
use Twig\Source;
use Twig\Template;
use Twig\\{$parentTemplate};
/* foo.twig */
class __TwigTemplate_%x extends Template
class __TwigTemplate_%x extends $parentTemplate
{
private \$source;
private \$macros = [];
@@ -173,7 +175,7 @@ class __TwigTemplate_%x extends Template
\$macros["macro"] = \$this->macros["macro"] = \$this->loadTemplate("foo.twig", "foo.twig", 2)->unwrap();
// line 1
\$this->parent = \$this->loadTemplate("layout.twig", "foo.twig", 1);
\$this->parent->display(\$context, array_merge(\$this->blocks, \$blocks));
{$this->getDisplayOrYield('$this->parent')}(\$context, array_merge(\$this->blocks, \$blocks));
}
/**
@@ -216,6 +218,7 @@ EOF
new ConstantExpression('foo', 2),
2
);
$parentTemplate = $this->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template';
$twig = new Environment($this->createMock(LoaderInterface::class), ['debug' => true]);
$node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source);
@@ -233,10 +236,10 @@ use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityNotAllowedFilterError;
use Twig\Sandbox\SecurityNotAllowedFunctionError;
use Twig\Source;
use Twig\Template;
use Twig\\{$parentTemplate};
/* foo.twig */
class __TwigTemplate_%x extends Template
class __TwigTemplate_%x extends $parentTemplate
{
private \$source;
private \$macros = [];
@@ -263,7 +266,7 @@ class __TwigTemplate_%x extends Template
// line 4
\$context["foo"] = "foo";
// line 2
\$this->getParent(\$context)->display(\$context, array_merge(\$this->blocks, \$blocks));
{$this->getDisplayOrYield('$this->getParent($context)')}(\$context, array_merge(\$this->blocks, \$blocks));
}
/**
+12
View File
@@ -11,6 +11,8 @@ namespace Twig\Tests\Node;
* file that was distributed with this source code.
*/
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\Node\Expression\AssignNameExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\NameExpression;
@@ -49,6 +51,15 @@ EOF
$names = new Node([new AssignNameExpression('foo', 1)], [], 1);
$values = new Node([new PrintNode(new ConstantExpression('foo', 1), 1)], [], 1);
$node = new SetNode(true, $names, $values, 1);
$tests[] = [$node, <<<EOF
// line 1
\$context["foo"] = ('' === \$tmp = implode('', iterator_to_array((function () use (&\$context, \$macros, \$blocks) {
echo "foo";
})() ?? new \EmptyIterator()))) ? '' : new Markup(\$tmp, \$this->env->getCharset());
EOF
, new Environment(new ArrayLoader(), ['use_yield' => true]),
];
$tests[] = [$node, <<<EOF
// line 1
\$context["foo"] = (function () use (&\$context, \$macros, \$blocks) {
@@ -62,6 +73,7 @@ EOF
}
})();
EOF
, new Environment(new ArrayLoader(), ['use_yield' => false]),
];
$names = new Node([new AssignNameExpression('foo', 1)], [], 1);
+1 -1
View File
@@ -58,7 +58,7 @@ class TemplateWrapperTest extends TestCase
{
$twig = new Environment(new ArrayLoader([
'index' => '{% block foo %}{{ foo }}{{ bar }}{% endblock %}',
]));
], ['use_yield' => false]));
$twig->addGlobal('bar', 'BAR');
$wrapper = $twig->load('index');