diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c9d618b4..76b52fbba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,14 +18,11 @@ jobs: runs-on: 'ubuntu-latest' - continue-on-error: ${{ matrix.experimental }} - strategy: matrix: php-version: - '8.2' - '8.3' - experimental: [false] steps: - name: "Checkout code" @@ -50,7 +47,7 @@ jobs: run: vendor/bin/simple-phpunit --version - name: "Run tests" - run: SYMFONY_DEPRECATIONS_HELPER=ignoreFile=./tests/ignore-use-yield-deprecations vendor/bin/simple-phpunit + run: vendor/bin/simple-phpunit extension-tests: needs: @@ -76,7 +73,6 @@ jobs: - 'markdown-extra' - 'string-extra' - 'twig-extra-bundle' - experimental: [false] steps: - name: "Checkout code" @@ -107,7 +103,7 @@ jobs: - name: "Run tests for ${{ matrix.extension }}" working-directory: extra/${{ matrix.extension }} - run: SYMFONY_DEPRECATIONS_HELPER=ignoreFile=../../tests/ignore-use-yield-deprecations ../../vendor/bin/simple-phpunit + run: ../../vendor/bin/simple-phpunit integration-tests: needs: diff --git a/composer.json b/composer.json index 18b72df56..0f1dc54dd 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,6 @@ ], "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "^1.3", "symfony/polyfill-ctype": "^1.8" }, diff --git a/src/Attribute/YieldReady.php b/src/Attribute/YieldReady.php new file mode 100644 index 000000000..7c178d1cc --- /dev/null +++ b/src/Attribute/YieldReady.php @@ -0,0 +1,24 @@ +env = $env; $this->reset(); - $this->checkForOutput = $env->isDebug(); } public function getEnvironment(): Environment @@ -69,9 +69,20 @@ class Compiler public function compile(Node $node, int $indentation = 0) { $this->reset($indentation); - $node->compile($this); + $this->didUseEchoStack[] = $this->didUseEcho; - return $this; + try { + $this->didUseEcho = false; + $node->compile($this); + + if ($this->didUseEcho) { + throw new \LogicException('Using "%s" is not supported; use "yield" instead in "%s".', $this->didUseEcho, \get_class($node)); + } + + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } } /** @@ -79,23 +90,24 @@ class Compiler */ public function subcompile(Node $node, bool $raw = true) { - if (false === $raw) { + if (!$raw) { $this->source .= str_repeat(' ', $this->indentation * 4); } - $node->compile($this); + $this->didUseEchoStack[] = $this->didUseEcho; - return $this; - } + try { + $this->didUseEcho = false; + $node->compile($this); - /** - * @return $this - */ - public function checkForOutput(bool $checkForOutput) - { - $this->checkForOutput = $checkForOutput ? $this->env->isDebug() : false; + if ($this->didUseEcho) { + throw new \LogicException(sprintf('Using "%s" is not supported; use "yield" instead in "%s".', $this->didUseEcho, \get_class($node))); + } - return $this; + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } } /** @@ -105,9 +117,7 @@ class Compiler */ public function raw(string $string) { - if ($this->checkForOutput) { - $this->checkStringForOutput(trim($string)); - } + $this->checkForEcho($string); $this->source .= $string; return $this; @@ -121,10 +131,7 @@ class Compiler public function write(...$strings) { foreach ($strings as $string) { - if ($this->checkForOutput) { - $this->checkStringForOutput(trim($string)); - } - + $this->checkForEcho($string); $this->source .= str_repeat(' ', $this->indentation * 4).$string; } @@ -241,12 +248,12 @@ class Compiler return sprintf('__internal_compile_%d', $this->varNameSalt++); } - private function checkStringForOutput(string $string): void + private function checkForEcho(string $string): void { - if (str_starts_with($string, 'echo')) { - trigger_deprecation('twig/twig', '3.9.0', 'Using "echo" in a "Node::compile()" method is deprecated; use a "TextNode" or "PrintNode" instead or use "yield" when "use_yield" is "true" on the environment (triggered by "%s").', $string); - } elseif (str_starts_with($string, 'print')) { - trigger_deprecation('twig/twig', '3.9.0', 'Using "print" in a "Node::compile()" method is deprecated; use a "TextNode" or "PrintNode" instead or use "yield" when "use_yield" is "true" on the environment (triggered by "%s").', $string); + if ($this->didUseEcho) { + return; } + + $this->didUseEcho = preg_match('/^\s*+(echo|print)\b/', $string, $m) ? $m[1] : false; } } diff --git a/src/Node/AutoEscapeNode.php b/src/Node/AutoEscapeNode.php index cd970411b..f9bc17e07 100644 --- a/src/Node/AutoEscapeNode.php +++ b/src/Node/AutoEscapeNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -24,6 +25,7 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class AutoEscapeNode extends Node { public function __construct($value, Node $body, int $lineno, string $tag = 'autoescape') diff --git a/src/Node/BlockNode.php b/src/Node/BlockNode.php index e92a70ff1..262e50688 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -37,14 +37,7 @@ class BlockNode extends Node $compiler ->subcompile($this->getNode('body')) - ; - - if (!$this->getNode('body') instanceof NodeOutputInterface) { - // needed when body doesn't yield anything - $compiler->write("yield '';\n"); - } - - $compiler + ->write("return; yield '';\n") // needed when body doesn't yield anything ->outdent() ->write("}\n\n") ; diff --git a/src/Node/BlockReferenceNode.php b/src/Node/BlockReferenceNode.php index 63e2b5ea1..f48082be3 100644 --- a/src/Node/BlockReferenceNode.php +++ b/src/Node/BlockReferenceNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,6 +20,7 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class BlockReferenceNode extends Node implements NodeOutputInterface { public function __construct(string $name, int $lineno, ?string $tag = null) diff --git a/src/Node/BodyNode.php b/src/Node/BodyNode.php index 041cbf685..08115b3bd 100644 --- a/src/Node/BodyNode.php +++ b/src/Node/BodyNode.php @@ -11,11 +11,14 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; + /** * Represents a body node. * * @author Fabien Potencier */ +#[YieldReady] class BodyNode extends Node { } diff --git a/src/Node/CheckSecurityCallNode.php b/src/Node/CheckSecurityCallNode.php index a78a38d80..d5f457618 100644 --- a/src/Node/CheckSecurityCallNode.php +++ b/src/Node/CheckSecurityCallNode.php @@ -11,11 +11,13 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** * @author Fabien Potencier */ +#[YieldReady] class CheckSecurityCallNode extends Node { public function compile(Compiler $compiler) diff --git a/src/Node/CheckSecurityNode.php b/src/Node/CheckSecurityNode.php index c5dc38021..9df4ca913 100644 --- a/src/Node/CheckSecurityNode.php +++ b/src/Node/CheckSecurityNode.php @@ -11,11 +11,13 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** * @author Fabien Potencier */ +#[YieldReady] class CheckSecurityNode extends Node { private array $usedFilters; diff --git a/src/Node/CheckToStringNode.php b/src/Node/CheckToStringNode.php index c7a9d6984..81fb92404 100644 --- a/src/Node/CheckToStringNode.php +++ b/src/Node/CheckToStringNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -24,6 +25,7 @@ use Twig\Node\Expression\AbstractExpression; * * @author Fabien Potencier */ +#[YieldReady] class CheckToStringNode extends AbstractExpression { public function __construct(AbstractExpression $expr) diff --git a/src/Node/DeprecatedNode.php b/src/Node/DeprecatedNode.php index ff9fcb4d6..2dc425dd3 100644 --- a/src/Node/DeprecatedNode.php +++ b/src/Node/DeprecatedNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -20,6 +21,7 @@ use Twig\Node\Expression\ConstantExpression; * * @author Yonel Ceruto */ +#[YieldReady] class DeprecatedNode extends Node { public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) diff --git a/src/Node/DoNode.php b/src/Node/DoNode.php index bf979dae7..445016ab2 100644 --- a/src/Node/DoNode.php +++ b/src/Node/DoNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -19,6 +20,7 @@ use Twig\Node\Expression\AbstractExpression; * * @author Fabien Potencier */ +#[YieldReady] class DoNode extends Node { public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) diff --git a/src/Node/EmbedNode.php b/src/Node/EmbedNode.php index ce95f3a39..545509462 100644 --- a/src/Node/EmbedNode.php +++ b/src/Node/EmbedNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -20,6 +21,7 @@ use Twig\Node\Expression\ConstantExpression; * * @author Fabien Potencier */ +#[YieldReady] class EmbedNode extends IncludeNode { // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module) diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index c9d836726..13e72df17 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -38,10 +38,9 @@ class BlockReferenceExpression extends AbstractExpression $this->compileTemplateCall($compiler, 'hasBlock'); } else { if ($this->getAttribute('output')) { - $compiler - ->addDebugInfo($this) - ->write('yield from ') - ; + $compiler->addDebugInfo($this); + + $compiler->write('yield from '); $this ->compileTemplateCall($compiler, 'yieldBlock') ->raw(";\n"); @@ -67,10 +66,7 @@ class BlockReferenceExpression extends AbstractExpression ; } - $compiler - ->raw('->unwrap()') - ->raw(sprintf('->%s', $method)) - ; + $compiler->raw(sprintf('->unwrap()->%s', $method)); return $this->compileBlockArguments($compiler); } diff --git a/src/Node/FlushNode.php b/src/Node/FlushNode.php index fa50a88ee..8a3dde6fc 100644 --- a/src/Node/FlushNode.php +++ b/src/Node/FlushNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,6 +19,7 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class FlushNode extends Node { public function __construct(int $lineno, string $tag) diff --git a/src/Node/ForLoopNode.php b/src/Node/ForLoopNode.php index 9120b962f..503687c2b 100644 --- a/src/Node/ForLoopNode.php +++ b/src/Node/ForLoopNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,6 +19,7 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class ForLoopNode extends Node { public function __construct(int $lineno, ?string $tag = null) diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index 80997a9d9..49f4e863f 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\AssignNameExpression; @@ -21,6 +22,7 @@ use Twig\Node\Expression\AssignNameExpression; * * @author Fabien Potencier */ +#[YieldReady] class ForNode extends Node { private ForLoopNode $loop; diff --git a/src/Node/IfNode.php b/src/Node/IfNode.php index 940e5deab..1b883305a 100644 --- a/src/Node/IfNode.php +++ b/src/Node/IfNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,6 +20,7 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class IfNode extends Node { public function __construct(Node $tests, ?Node $else, int $lineno, ?string $tag = null) diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index 1a3494c91..db47bfe61 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\NameExpression; @@ -20,6 +21,7 @@ use Twig\Node\Expression\NameExpression; * * @author Fabien Potencier */ +#[YieldReady] class ImportNode extends Node { public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, ?string $tag = null, bool $global = true) diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index b7fcda306..abc0f3546 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -20,6 +21,7 @@ use Twig\Node\Expression\AbstractExpression; * * @author Fabien Potencier */ +#[YieldReady] class IncludeNode extends Node implements NodeOutputInterface { public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, ?string $tag = null) @@ -68,14 +70,9 @@ class IncludeNode extends Node implements NodeOutputInterface ->write("}\n") ; } else { - $compiler - ->write('yield from ') - ; - + $compiler->write('yield from '); $this->addGetTemplate($compiler); - $compiler->raw('->unwrap()->yield('); - $this->addTemplateArguments($compiler); $compiler->raw(");\n"); } diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index ae62a22b5..e44150f52 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Error\SyntaxError; @@ -19,6 +20,7 @@ use Twig\Error\SyntaxError; * * @author Fabien Potencier */ +#[YieldReady] class MacroNode extends Node { public const VARARGS_NAME = 'varargs'; diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index b0e14ff6b..6aa4f8fb2 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -26,6 +27,7 @@ use Twig\Source; * * @author Fabien Potencier */ +#[YieldReady] final class ModuleNode extends Node { public function __construct(Node $body, ?AbstractExpression $parent, Node $blocks, Node $macros, Node $traits, $embeddedTemplates, Source $source) @@ -334,9 +336,8 @@ final class ModuleNode extends Node $compiler->raw('$this->getParent($context)'); } $compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\n"); - } elseif (!$this->hasNodeOutputNodes($this->getNode('body'))) { - // ensure at least one yield call even for templates with no output - $compiler->write("yield '';\n"); + } else { + $compiler->write("return; yield '';\n"); // ensure at least one yield call even for templates with no output } $compiler diff --git a/src/Node/Node.php b/src/Node/Node.php index d043196f7..faacd9310 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Source; @@ -20,6 +21,7 @@ use Twig\Source; * * @author Fabien Potencier */ +#[YieldReady] class Node implements \Countable, \IteratorAggregate { protected $nodes; @@ -82,7 +84,7 @@ class Node implements \Countable, \IteratorAggregate public function compile(Compiler $compiler) { foreach ($this->nodes as $node) { - $node->compile($compiler); + $compiler->subcompile($node); } } diff --git a/src/Node/PrintNode.php b/src/Node/PrintNode.php index 68c565d8f..a6a89bd74 100644 --- a/src/Node/PrintNode.php +++ b/src/Node/PrintNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -20,6 +21,7 @@ use Twig\Node\Expression\AbstractExpression; * * @author Fabien Potencier */ +#[YieldReady] class PrintNode extends Node implements NodeOutputInterface { public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) @@ -29,8 +31,9 @@ class PrintNode extends Node implements NodeOutputInterface public function compile(Compiler $compiler): void { + $compiler->addDebugInfo($this); + $compiler - ->addDebugInfo($this) ->write('yield ') ->subcompile($this->getNode('expr')) ->raw(";\n") diff --git a/src/Node/SandboxNode.php b/src/Node/SandboxNode.php index 0ffef6dbb..80aecbdba 100644 --- a/src/Node/SandboxNode.php +++ b/src/Node/SandboxNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,6 +19,7 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class SandboxNode extends Node { public function __construct(Node $body, int $lineno, ?string $tag = null) diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 7dea50023..6b4c873e1 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\ConstantExpression; @@ -19,6 +20,7 @@ use Twig\Node\Expression\ConstantExpression; * * @author Fabien Potencier */ +#[YieldReady] class SetNode extends Node implements NodeCaptureInterface { public function __construct(bool $capture, Node $names, Node $values, int $lineno, ?string $tag = null) diff --git a/src/Node/TextNode.php b/src/Node/TextNode.php index b3335e438..fae65fb2c 100644 --- a/src/Node/TextNode.php +++ b/src/Node/TextNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,6 +20,7 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class TextNode extends Node implements NodeOutputInterface { public function __construct(string $data, int $lineno) @@ -28,8 +30,9 @@ class TextNode extends Node implements NodeOutputInterface public function compile(Compiler $compiler): void { + $compiler->addDebugInfo($this); + $compiler - ->addDebugInfo($this) ->write('yield ') ->string($this->getAttribute('data')) ->raw(";\n") diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index 3dd2b07ec..9b8c57884 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,6 +19,7 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class WithNode extends Node { public function __construct(Node $body, ?Node $variables, bool $only, int $lineno, ?string $tag = null) diff --git a/src/Template.php b/src/Template.php index 182bf3942..43311999c 100644 --- a/src/Template.php +++ b/src/Template.php @@ -81,7 +81,7 @@ abstract class Template * This method is for internal use only and should never be called * directly. * - * @return Template|TemplateWrapper|false The parent template or false if there is no parent + * @return self|TemplateWrapper|false The parent template or false if there is no parent */ public function getParent(array $context) { @@ -90,9 +90,7 @@ abstract class Template } try { - $parent = $this->doGetParent($context); - - if (false === $parent) { + if (!$parent = $this->doGetParent($context)) { return false; } @@ -123,6 +121,86 @@ abstract class Template return true; } + /** + * Displays 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 displayParentBlock($name, array $context, array $blocks = []) + { + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + echo $data; + } + } + + /** + * Displays a block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to display + * @param array $context The context + * @param array $blocks The current set of blocks + * @param bool $useBlocks Whether to use the current set of blocks + */ + public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null) + { + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) { + echo $data; + } + } + + /** + * Renders a parent block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to render from the parent + * @param array $context The context + * @param array $blocks The current set of blocks + * + * @return string The rendered block + */ + public function renderParentBlock($name, array $context, array $blocks = []) + { + $content = ''; + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + $content .= $data; + } + + return $content; + } + + /** + * Renders a block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to render + * @param array $context The context + * @param array $blocks The current set of blocks + * @param bool $useBlocks Whether to use the current set of blocks + * + * @return string The rendered block + */ + public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true) + { + $content = ''; + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) { + $content .= $data; + } + + return $content; + } + /** * Returns whether a block exists or not in the current context of the template. * @@ -145,7 +223,7 @@ abstract class Template return true; } - if (false !== $parent = $this->getParent($context)) { + if ($parent = $this->getParent($context)) { return $parent->hasBlock($name, $context); } @@ -167,7 +245,7 @@ abstract class Template { $names = array_merge(array_keys($blocks), array_keys($this->blocks)); - if (false !== $parent = $this->getParent($context)) { + if ($parent = $this->getParent($context)) { $names = array_merge($names, $parent->getBlockNames($context)); } @@ -175,7 +253,7 @@ abstract class Template } /** - * @return Template|TemplateWrapper + * @return self|TemplateWrapper */ protected function loadTemplate($template, $templateName = null, $line = null, $index = null) { @@ -220,7 +298,7 @@ abstract class Template /** * @internal * - * @return Template + * @return self */ public function unwrap() { @@ -240,6 +318,23 @@ abstract class Template return $this->blocks; } + public function display(array $context, array $blocks = []): void + { + foreach ($this->yield($context, $blocks) as $data) { + echo $data; + } + } + + public function render(array $context): string + { + $content = ''; + foreach ($this->yield($context) as $data) { + $content .= $data; + } + + return $content; + } + /** * @return iterable */ @@ -270,27 +365,10 @@ abstract class Template } } - public function render(array $context): string - { - $content = ''; - foreach ($this->yield($context) as $data) { - $content .= $data; - } - - return $content; - } - - public function display(array $context, array $blocks = []): void - { - foreach ($this->yield($context, $blocks) as $data) { - echo $data; - } - } - /** * @return iterable */ - public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, Template $templateContext = null) + public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null) { if ($useBlocks && isset($blocks[$name])) { $template = $blocks[$name][0]; @@ -304,7 +382,7 @@ abstract class Template } // avoid RCEs when sandbox is enabled - if (null !== $template && !$template instanceof Template) { + if (null !== $template && !$template instanceof self) { throw new \LogicException('A block must be a method on a \Twig\Template instance.'); } @@ -329,8 +407,7 @@ abstract class Template throw $e; } - } elseif (false !== $parent = $this->getParent($context)) { - /** @var Template $parent */ + } elseif ($parent = $this->getParent($context)) { 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()); @@ -339,29 +416,6 @@ abstract class Template } } - /** - * Renders a block. - * - * This method is for internal use only and should never be called - * directly. - * - * @param string $name The block name to render - * @param array $context The context - * @param array $blocks The current set of blocks - * @param bool $useBlocks Whether to use the current set of blocks - * - * @return string The rendered block - */ - 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. * @@ -378,34 +432,10 @@ abstract class Template { 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 Template $parent */ - yield from $parent->yieldBlock($name, $context, $blocks, false); + } elseif ($parent = $this->getParent($context)) { + yield from $parent->unwrap()->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()); } } - - /** - * Renders a parent block. - * - * This method is for internal use only and should never be called - * directly. - * - * @param string $name The block name to render from the parent - * @param array $context The context - * @param array $blocks The current set of blocks - * - * @return string The rendered block - */ - public function renderParentBlock($name, array $context, array $blocks = []) - { - $content = ''; - foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { - $content .= $data; - } - - return $content; - } } diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index 0ef066631..30d6810f8 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -67,19 +67,4 @@ abstract class NodeTestCase extends TestCase { return 'CoreExtension::getAttribute($this->env, $this->source, '; } - - protected function getEchoOrYield(): string - { - return 'yield'; - } - - protected function getDisplayOrYield(string $expr): string - { - return sprintf('yield from %s->unwrap()->yield', $expr); - } - - protected function getDisplayOrYieldBlock(string $expr): string - { - return sprintf('yield from %s->unwrap()->yieldBlock', $expr); - } } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 549b40e41..6e89e4b66 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -16,6 +16,7 @@ use Twig\Cache\CacheInterface; use Twig\Cache\FilesystemCache; use Twig\Environment; use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; use Twig\Extension\AbstractExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; @@ -403,6 +404,19 @@ class EnvironmentTest extends TestCase $this->assertSame('dynamic', $parser->getTag()); } + public function testLegacyEchoingNode() + { + $loader = new ArrayLoader(['echo_bar' => 'A{% set v %}B{% test %}C{% endset %}D{% test %}E{{ v }}F']); + + $twig = new Environment($loader); + $twig->addExtension(new EnvironmentTest_Extension()); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('An exception has been thrown during the compilation of a template ("Using "echo" is not supported; use "yield" instead in "Twig\Tests\EnvironmentTest_LegacyEchoingNode".") in "echo_bar".'); + + $this->assertSame('ADbarEBbarCF', $twig->render('echo_bar')); + } + protected function getMockLoader($templateName, $templateContent) { $loader = $this->createMock(LoaderInterface::class); @@ -486,6 +500,9 @@ class EnvironmentTest_TokenParser extends AbstractTokenParser { public function parse(Token $token): Node { + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + + return new EnvironmentTest_LegacyEchoingNode(); } public function getTag(): string @@ -530,3 +547,14 @@ class EnvironmentTest_Runtime return $name; } } + +class EnvironmentTest_LegacyEchoingNode extends Node +{ + public function compile($compiler) + { + $compiler + ->addDebugInfo($this) + ->write('echo "bar";') + ; + } +} diff --git a/tests/Node/AutoEscapeTest.php b/tests/Node/AutoEscapeTest.php index b2df9b160..9cf18742b 100644 --- a/tests/Node/AutoEscapeTest.php +++ b/tests/Node/AutoEscapeTest.php @@ -31,10 +31,9 @@ class AutoEscapeTest extends NodeTestCase { $body = new Node([new TextNode('foo', 1)]); $node = new AutoEscapeNode(true, $body, 1); - $displayStmt = $this->getEchoOrYield(); return [ - [$node, "// line 1\n$displayStmt \"foo\";"], + [$node, "// line 1\nyield \"foo\";"], ]; } } diff --git a/tests/Node/BlockReferenceTest.php b/tests/Node/BlockReferenceTest.php index f291f29f3..1211ee17b 100644 --- a/tests/Node/BlockReferenceTest.php +++ b/tests/Node/BlockReferenceTest.php @@ -26,9 +26,9 @@ class BlockReferenceTest extends NodeTestCase public function getTests() { return [ - [new BlockReferenceNode('foo', 1), <<getDisplayOrYieldBlock('$this')}('foo', \$context, \$blocks); +yield from $this->unwrap()->yieldBlock('foo', $context, $blocks); EOF ], ]; diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index 712360b52..8b25ab22c 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -38,9 +38,10 @@ public function block_foo(\$context, array \$blocks = []) { \$macros = \$this->macros; yield "foo"; + return; yield ''; } EOF - , new Environment(new ArrayLoader()), + , new Environment(new ArrayLoader()), ]; $tests[] = [new BlockNode('foo', new Node(), 1), <<macros; - yield ''; + return; yield ''; } EOF - , new Environment(new ArrayLoader()), + , new Environment(new ArrayLoader()), ]; return $tests; diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index 5b6a3ee4f..3ca4b22a3 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -53,14 +53,13 @@ class ForTest extends NodeTestCase $else = null; $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', false); - $displayStmt = $this->getEchoOrYield(); $tests[] = [$node, <<getVariableGetter('items')}); foreach (\$context['_seq'] as \$context["key"] => \$context["item"]) { - $displayStmt {$this->getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; } \$_parent = \$context['_parent']; unset(\$context['_seq'], \$context['_iterated'], \$context['key'], \$context['item'], \$context['_parent'], \$context['loop']); @@ -94,7 +93,7 @@ if (is_array(\$context['_seq']) || (is_object(\$context['_seq']) && \$context['_ \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - $displayStmt {$this->getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; @@ -136,7 +135,7 @@ if (is_array(\$context['_seq']) || (is_object(\$context['_seq']) && \$context['_ \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - $displayStmt {$this->getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; @@ -179,7 +178,7 @@ if (is_array(\$context['_seq']) || (is_object(\$context['_seq']) && \$context['_ \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - $displayStmt {$this->getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; \$context['_iterated'] = true; ++\$context['loop']['index0']; ++\$context['loop']['index']; @@ -191,7 +190,7 @@ foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { } } if (!\$context['_iterated']) { - $displayStmt {$this->getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; } \$_parent = \$context['_parent']; unset(\$context['_seq'], \$context['_iterated'], \$context['k'], \$context['v'], \$context['_parent'], \$context['loop']); diff --git a/tests/Node/IfTest.php b/tests/Node/IfTest.php index 5dda061d0..26821a39b 100644 --- a/tests/Node/IfTest.php +++ b/tests/Node/IfTest.php @@ -47,12 +47,11 @@ class IfTest extends NodeTestCase ], [], 1); $else = null; $node = new IfNode($t, $else, 1); - $displayStmt = $this->getEchoOrYield(); $tests[] = [$node, <<getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; } EOF ]; @@ -69,9 +68,9 @@ EOF $tests[] = [$node, <<getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; } elseif (false) { - $displayStmt {$this->getVariableGetter('bar')}; + yield {$this->getVariableGetter('bar')}; } EOF ]; @@ -86,9 +85,9 @@ EOF $tests[] = [$node, <<getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; } else { - $displayStmt {$this->getVariableGetter('bar')}; + yield {$this->getVariableGetter('bar')}; } EOF ]; diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index ee68339c5..cda9d7bf2 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -40,9 +40,9 @@ class IncludeTest extends NodeTestCase $expr = new ConstantExpression('foo.twig', 1); $node = new IncludeNode($expr, null, false, false, 1); - $tests[] = [$node, <<getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(\$context); +yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield($context); EOF ]; @@ -53,25 +53,25 @@ EOF 0 ); $node = new IncludeNode($expr, null, false, false, 1); - $tests[] = [$node, <<getDisplayOrYield('$this->loadTemplate(((true) ? ("foo") : ("foo")), null, 1)')}(\$context); +yield from $this->loadTemplate(((true) ? ("foo") : ("foo")), null, 1)->unwrap()->yield($context); EOF ]; $expr = new ConstantExpression('foo.twig', 1); $vars = new ArrayExpression([new ConstantExpression('foo', 1), new ConstantExpression(true, 1)], 1); $node = new IncludeNode($expr, $vars, false, false, 1); - $tests[] = [$node, <<getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(CoreExtension::arrayMerge(\$context, ["foo" => true])); +yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield(CoreExtension::arrayMerge($context, ["foo" => true])); EOF ]; $node = new IncludeNode($expr, $vars, true, false, 1); - $tests[] = [$node, <<getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(CoreExtension::toArray(["foo" => true])); +yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield(CoreExtension::toArray(["foo" => true])); EOF ]; @@ -85,7 +85,7 @@ try { // ignore missing template } if (\$__internal_%s) { - {$this->getDisplayOrYield('$__internal_%s')}(CoreExtension::toArray(["foo" => true])); + yield from \$__internal_%s->unwrap()->yield(CoreExtension::toArray(["foo" => true])); } EOF , null, true]; diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 1e2639607..09d7ee6ca 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -63,7 +63,7 @@ public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) })() ?? new \EmptyIterator())), \$this->env->getCharset()); } EOF - , new Environment(new ArrayLoader()), + , new Environment(new ArrayLoader()), ]; return $tests; diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index fd23b3cc8..c4dcaa1e3 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -55,7 +55,6 @@ class ModuleTest extends NodeTestCase $macros = new Node(); $traits = new Node(); $source = new Source('{{ foo }}', 'foo.twig'); - $displayStmt = $this->getEchoOrYield(); $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); $tests[] = [$node, <<macros; // line 1 - $displayStmt "foo"; + yield "foo"; + return; yield ''; } /** @@ -174,7 +174,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->getDisplayOrYield('$this->parent')}(\$context, array_merge(\$this->blocks, \$blocks)); + yield from \$this->parent->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks)); } /** @@ -264,7 +264,7 @@ class __TwigTemplate_%x extends Template // line 4 \$context["foo"] = "foo"; // line 2 - {$this->getDisplayOrYield('$this->getParent($context)')}(\$context, array_merge(\$this->blocks, \$blocks)); + yield from \$this->getParent(\$context)->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks)); } /** diff --git a/tests/Node/PrintTest.php b/tests/Node/PrintTest.php index f951c2e36..2df440c28 100644 --- a/tests/Node/PrintTest.php +++ b/tests/Node/PrintTest.php @@ -28,9 +28,7 @@ class PrintTest extends NodeTestCase public function getTests() { $tests = []; - $displayStmt = $this->getEchoOrYield(); - - $tests[] = [new PrintNode(new ConstantExpression('foo', 1), 1), "// line 1\n$displayStmt \"foo\";"]; + $tests[] = [new PrintNode(new ConstantExpression('foo', 1), 1), "// line 1\nyield \"foo\";"]; return $tests; } diff --git a/tests/Node/SandboxTest.php b/tests/Node/SandboxTest.php index bf16f1f03..c74feba42 100644 --- a/tests/Node/SandboxTest.php +++ b/tests/Node/SandboxTest.php @@ -31,7 +31,6 @@ class SandboxTest extends NodeTestCase $body = new TextNode('foo', 1); $node = new SandboxNode($body, 1); - $displayStmt = $this->getEchoOrYield(); $tests[] = [$node, <<sandbox->isSandboxed()) { \$this->sandbox->enableSandbox(); } try { - $displayStmt "foo"; + yield "foo"; } finally { if (!\$alreadySandboxed) { \$this->sandbox->disableSandbox(); diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index 9c105d88d..d193ac4e4 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -73,9 +73,9 @@ EOF $names = new Node([new AssignNameExpression('foo', 1), new AssignNameExpression('bar', 1)], [], 1); $values = new Node([new ConstantExpression('foo', 1), new NameExpression('bar', 1)], [], 1); $node = new SetNode(false, $names, $values, 1); - $tests[] = [$node, <<getVariableGetter('bar')}]; +[$context["foo"], $context["bar"]] = ["foo", ($context["bar"] ?? null)]; EOF ]; diff --git a/tests/Node/TextTest.php b/tests/Node/TextTest.php index 31639cc2d..357362c3c 100644 --- a/tests/Node/TextTest.php +++ b/tests/Node/TextTest.php @@ -26,8 +26,7 @@ class TextTest extends NodeTestCase public function getTests() { $tests = []; - $displayStmt = $this->getEchoOrYield(); - $tests[] = [new TextNode('foo', 1), "// line 1\n$displayStmt \"foo\";"]; + $tests[] = [new TextNode('foo', 1), "// line 1\nyield \"foo\";"]; return $tests; } diff --git a/tests/TemplateWrapperTest.php b/tests/TemplateWrapperTest.php index 803fed00a..a302aba05 100644 --- a/tests/TemplateWrapperTest.php +++ b/tests/TemplateWrapperTest.php @@ -53,4 +53,20 @@ class TemplateWrapperTest extends TestCase $wrapper = $twig->load('index'); $this->assertEquals('FOOBAR', $wrapper->renderBlock('foo', ['foo' => 'FOO'])); } + + public function testDisplayBlock() + { + $twig = new Environment(new ArrayLoader([ + 'index' => '{% block foo %}{{ foo }}{{ bar }}{% endblock %}', + ])); + + $twig->addGlobal('bar', 'BAR'); + + $wrapper = $twig->load('index'); + + ob_start(); + $wrapper->displayBlock('foo', ['foo' => 'FOO']); + + $this->assertEquals('FOOBAR', ob_get_clean()); + } } diff --git a/tests/ignore-use-yield-deprecations b/tests/ignore-use-yield-deprecations deleted file mode 100644 index 0f8442115..000000000 --- a/tests/ignore-use-yield-deprecations +++ /dev/null @@ -1 +0,0 @@ -%Since twig/twig 3.9.0: Not setting "use_yield" to "true" is deprecated.%