diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6274bd1fe29..b22266b7ab1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,12 +14,10 @@ permissions: jobs: tests: - name: "PHP ${{ matrix.php-version }} (yield: ${{ matrix.use_yield }})" + name: "PHP ${{ matrix.php-version }}" runs-on: 'ubuntu-latest' - continue-on-error: ${{ matrix.experimental }} - strategy: matrix: php-version: @@ -30,8 +28,6 @@ jobs: - '8.1' - '8.2' - '8.3' - experimental: [false] - use_yield: [true, false] steps: - name: "Checkout code" @@ -49,8 +45,8 @@ jobs: - run: composer install - - name: "Switch use_yield to true" - if: ${{ matrix.use_yield }} + - name: "Switch use_yield to true on PHP ${{ matrix.php-version }}" + if: "matrix.php-version == '8.2'" run: | sed -i -e "s/'use_yield' => false/'use_yield' => true/" src/Environment.php @@ -61,13 +57,13 @@ 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: - 'tests' - name: "${{ matrix.extension }} PHP ${{ matrix.php-version }} (yield: ${{ matrix.use_yield }})" + name: "${{ matrix.extension }} PHP ${{ matrix.php-version }}" runs-on: 'ubuntu-latest' @@ -92,8 +88,6 @@ jobs: - 'markdown-extra' - 'string-extra' - 'twig-extra-bundle' - experimental: [false] - use_yield: [true, false] steps: - name: "Checkout code" @@ -118,18 +112,18 @@ jobs: - name: "PHPUnit version" run: vendor/bin/simple-phpunit --version - - name: "Composer install ${{ matrix.extension}}" - working-directory: extra/${{ matrix.extension}} + - name: "Composer install ${{ matrix.extension }}" + working-directory: extra/${{ matrix.extension }} run: composer install - name: "Switch use_yield to true" - if: ${{ matrix.use_yield }} + if: "matrix.php == '8.2'" 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}}" + - 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/CHANGELOG b/CHANGELOG index d0bc0cae33c..a6e7a9365ee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,9 @@ # 3.9.0 (2024-XX-XX) - * Add a new "yield" mode for output generation - The "use_yield" Environment option controls the output strategy: use "false" for "echo", "true" for "yield" + * Add a new "yield" mode for output generation; + Node implementations that use "echo" or "print" should use "yield" instead; + all Node implementations should be flagged with `#[YieldReady]` once they've been made ready for "yield"; + the "use_yield" Environment option can be turned on when all nodes have been made `#[YieldReady]`; "yield" will be the only strategy supported in the next major version * Add return type for Symfony 7 compatibility * Fix premature loop exit in Security Policy lookup of allowed methods/properties diff --git a/src/Attribute/YieldReady.php b/src/Attribute/YieldReady.php new file mode 100644 index 00000000000..335c4351e58 --- /dev/null +++ b/src/Attribute/YieldReady.php @@ -0,0 +1,20 @@ +env = $env; - $this->checkForOutput = $env->isDebug(); } public function getEnvironment(): Environment @@ -68,9 +68,20 @@ public function reset(int $indentation = 0) 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) { + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node)); + } + + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } } /** @@ -78,23 +89,24 @@ public function compile(Node $node, int $indentation = 0) */ 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) { + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node)); + } - return $this; + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } } /** @@ -104,9 +116,7 @@ public function checkForOutput(bool $checkForOutput) */ public function raw(string $string) { - if ($this->checkForOutput) { - $this->checkStringForOutput(trim($string)); - } + $this->checkForEcho($string); $this->source .= $string; return $this; @@ -120,10 +130,7 @@ public function raw(string $string) 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; } @@ -240,12 +247,12 @@ public function getVarName(): string 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/Environment.php b/src/Environment.php index f05de27743d..c6ba8b2c0fe 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -22,6 +22,7 @@ use Twig\Extension\EscaperExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\OptimizerExtension; +use Twig\Extension\YieldNotReadyExtension; use Twig\Loader\ArrayLoader; use Twig\Loader\ChainLoader; use Twig\Loader\LoaderInterface; @@ -100,7 +101,7 @@ class Environment * (default to -1 which means that all optimizations are enabled; * set it to 0 to disable). * - * * use_yield: Enable a new mode where template are using "yield" instead of "echo" + * * use_yield: Enable templates to exclusively use "yield" instead of "echo" * (default to "false", but switch it to "true" when possible * as this will be the only supported mode in Twig 4.0) */ @@ -120,10 +121,6 @@ public function __construct(LoaderInterface $loader, $options = []) ], $options); $this->useYield = (bool) $options['use_yield']; - if (!$this->useYield) { - trigger_deprecation('twig/twig', '3.9.0', 'Not setting "use_yield" to "true" is deprecated.'); - } - $this->debug = (bool) $options['debug']; $this->setCharset($options['charset'] ?? 'UTF-8'); $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload']; @@ -133,6 +130,9 @@ public function __construct(LoaderInterface $loader, $options = []) $this->addExtension(new CoreExtension()); $this->addExtension(new EscaperExtension($options['autoescape'])); + if (\PHP_VERSION_ID >= 80000) { + $this->addExtension(new YieldNotReadyExtension($this->useYield)); + } $this->addExtension(new OptimizerExtension($options['optimizations'])); } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 3a3caa9fe0c..b11e2e5e237 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1841,4 +1841,35 @@ public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $ty throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); } } + + /** + * @internal to be removed in Twig 4 + */ + public static function captureOutput(iterable $body): string + { + $output = ''; + $level = ob_get_level(); + ob_start(); + + try { + foreach ($body as $data) { + if (ob_get_length()) { + $output .= ob_get_clean(); + ob_start(); + } + + $output .= $data; + } + + if (ob_get_length()) { + $output .= ob_get_clean(); + } + } finally { + while (ob_get_level() > $level) { + ob_end_clean(); + } + } + + return $output; + } } diff --git a/src/Extension/YieldNotReadyExtension.php b/src/Extension/YieldNotReadyExtension.php new file mode 100644 index 00000000000..2503c8d8140 --- /dev/null +++ b/src/Extension/YieldNotReadyExtension.php @@ -0,0 +1,32 @@ +useYield = $useYield; + } + + public function getNodeVisitors(): array + { + return [new YieldNotReadyNodeVisitor($this->useYield)]; + } +} diff --git a/src/Node/AutoEscapeNode.php b/src/Node/AutoEscapeNode.php index cd970411b8d..f9bc17e078f 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 @@ * * @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 e20eae252bc..65174c02c6f 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class BlockNode extends Node { public function __construct(string $name, Node $body, int $lineno, ?string $tag = null) @@ -37,14 +39,7 @@ public function compile(Compiler $compiler): void $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 + ->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 8abb6f954f2..f48082be36d 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 @@ * * @author Fabien Potencier */ +#[YieldReady] class BlockReferenceNode extends Node implements NodeOutputInterface { public function __construct(string $name, int $lineno, ?string $tag = null) @@ -28,16 +30,9 @@ public function __construct(string $name, int $lineno, ?string $tag = null) public function compile(Compiler $compiler): void { - 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'))) - ; - } + $compiler + ->addDebugInfo($this) + ->write(sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) + ; } } diff --git a/src/Node/BodyNode.php b/src/Node/BodyNode.php index 041cbf685b1..08115b3bd00 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/CaptureNode.php b/src/Node/CaptureNode.php index 53ccce7f8e5..27ae4c60537 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,6 +19,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class CaptureNode extends Node { public function __construct(Node $body, int $lineno, ?string $tag = null) @@ -27,62 +29,29 @@ public function __construct(Node $body, int $lineno, ?string $tag = null) 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(';'); + $useYield = $compiler->getEnvironment()->useYield(); - return; + if (!$this->getAttribute('raw')) { + $compiler->raw("('' === \$tmp = "); } - + $compiler->raw($useYield ? "implode('', iterator_to_array(" : "\\Twig\\Extension\\CoreExtension::captureOutput("); 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('})();') + ->write("})() ?? new \EmptyIterator())") ; + if ($useYield) { + $compiler->raw(')'); + } + if (!$this->getAttribute('raw')) { + $compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())"); + } + $compiler->raw(';'); } } diff --git a/src/Node/CheckSecurityCallNode.php b/src/Node/CheckSecurityCallNode.php index a78a38d80bb..d5f45761895 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 c531c4dd415..991a428a42d 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 $usedFilters; diff --git a/src/Node/CheckToStringNode.php b/src/Node/CheckToStringNode.php index c7a9d6984e7..81fb92404f0 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 @@ * * @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 ff9fcb4d630..2dc425dd301 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 @@ * * @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 bf979dae77b..445016ab285 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 @@ * * @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 ce95f3a3990..54550946215 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 @@ * * @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 9b187b9245e..13e72df17ce 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -40,16 +40,10 @@ public function compile(Compiler $compiler): void if ($this->getAttribute('output')) { $compiler->addDebugInfo($this); - if ($compiler->getEnvironment()->useYield()) { - $compiler->write('yield from '); - $this - ->compileTemplateCall($compiler, 'yieldBlock') - ->raw(";\n"); - } else { - $this - ->compileTemplateCall($compiler, 'displayBlock') - ->raw(";\n"); - } + $compiler->write('yield from '); + $this + ->compileTemplateCall($compiler, 'yieldBlock') + ->raw(";\n"); } else { $this->compileTemplateCall($compiler, 'renderBlock'); } @@ -72,11 +66,7 @@ private function compileTemplateCall(Compiler $compiler, string $method): Compil ; } - if ($compiler->getEnvironment()->useYield()) { - $compiler->raw('->unwrap()'); - } - - $compiler->raw(sprintf('->%s', $method)); + $compiler->raw(sprintf('->unwrap()->%s', $method)); return $this->compileBlockArguments($compiler); } diff --git a/src/Node/Expression/InlinePrint.php b/src/Node/Expression/InlinePrint.php index 725536a869d..0a3c2e4f9ec 100644 --- a/src/Node/Expression/InlinePrint.php +++ b/src/Node/Expression/InlinePrint.php @@ -26,19 +26,9 @@ public function __construct(Node $node, int $lineno) public function compile(Compiler $compiler): void { - if ($compiler->getEnvironment()->useYield()) { - $compiler - ->raw('yield ') - ->subcompile($this->getNode('node')) - ; - } else { - $compiler - ->checkForOutput(false) - ->raw('print(') - ->checkForOutput(true) - ->subcompile($this->getNode('node')) - ->raw(')') - ; - } + $compiler + ->raw('yield ') + ->subcompile($this->getNode('node')) + ; } } diff --git a/src/Node/Expression/ParentExpression.php b/src/Node/Expression/ParentExpression.php index 22457cc3b3f..59d833ac99d 100644 --- a/src/Node/Expression/ParentExpression.php +++ b/src/Node/Expression/ParentExpression.php @@ -28,36 +28,19 @@ public function __construct(string $name, int $lineno, ?string $tag = null) public function compile(Compiler $compiler): void { - 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)') - ; - } + if ($this->getAttribute('output')) { + $compiler + ->addDebugInfo($this) + ->write('yield from $this->yieldParentBlock(') + ->string($this->getAttribute('name')) + ->raw(", \$context, \$blocks);\n") + ; } else { - 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)') - ; - } + $compiler + ->raw('$this->renderParentBlock(') + ->string($this->getAttribute('name')) + ->raw(', $context, $blocks)') + ; } } } diff --git a/src/Node/FlushNode.php b/src/Node/FlushNode.php index fa50a88ee56..8a3dde6fcc2 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 @@ * * @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 9120b962fe9..503687c2b2e 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 @@ * * @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 f4df0c77de5..5222cf9bf9f 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 @@ * * @author Fabien Potencier */ +#[YieldReady] class ForNode extends Node { private $loop; diff --git a/src/Node/IfNode.php b/src/Node/IfNode.php index 940e5deab9d..1b883305ad4 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 @@ * * @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 1a3494c9179..db47bfe61c9 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 @@ * * @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 09c6622ec62..abc0f35460e 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 @@ * * @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) @@ -58,18 +60,9 @@ public function compile(Compiler $compiler): void ->write("}\n") ->write(sprintf("if ($%s) {\n", $template)) ->indent() + ->write(sprintf('yield from $%s->unwrap()->yield(', $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") @@ -77,20 +70,9 @@ public function compile(Compiler $compiler): void ->write("}\n") ; } else { - if ($compiler->getEnvironment()->useYield()) { - $compiler - ->write('yield from ') - ; - } - + $compiler->write('yield from '); $this->addGetTemplate($compiler); - - if ($compiler->getEnvironment()->useYield()) { - $compiler->raw('->unwrap()->yield('); - } else { - $compiler->raw('->display('); - } - + $compiler->raw('->unwrap()->yield('); $this->addTemplateArguments($compiler); $compiler->raw(");\n"); } diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index ae62a22b5af..e44150f526c 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 @@ * * @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 04a5326a15c..6a76ed1ea5d 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 @@ * * @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) @@ -151,14 +153,14 @@ protected function compileClassHeader(Compiler $compiler) ->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n") ->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n") ->write("use Twig\Source;\n") - ->write(sprintf("use Twig\%s;\n\n", $compiler->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template')) + ->write(sprintf("use Twig\%s;\n\n", '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(sprintf(" extends %s\n", $compiler->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template')) + ->raw(sprintf(" extends %s\n", 'Template')) ->write("{\n") ->indent() ->write("private \$source;\n") @@ -326,25 +328,16 @@ protected function compileDisplay(Compiler $compiler) ->raw(");\n") ; } - if ($compiler->getEnvironment()->useYield()) { - $compiler->write('yield from '); - } else { - $compiler->write(''); - } + $compiler->write('yield from '); 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"); - } - } elseif ($compiler->getEnvironment()->useYield() && !$this->hasNodeOutputNodes($this->getNode('body'))) { - // ensure at least one yield call even for templates with no output - $compiler->write("yield '';\n"); + $compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\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 7b90b6092a3..16591fcf5e5 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 @@ * * @author Fabien Potencier */ +#[YieldReady] class Node implements \Countable, \IteratorAggregate { protected $nodes; @@ -82,7 +84,7 @@ public function __toString() 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 369995f5c86..a6a89bd74c6 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 @@ * * @author Fabien Potencier */ +#[YieldReady] class PrintNode extends Node implements NodeOutputInterface { public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) @@ -31,17 +33,8 @@ public function compile(Compiler $compiler): void { $compiler->addDebugInfo($this); - if ($compiler->getEnvironment()->useYield()) { - $compiler->write('yield '); - } else { - $compiler - ->checkForOutput(false) - ->write('echo ') - ->checkForOutput(true) - ; - } - $compiler + ->write('yield ') ->subcompile($this->getNode('expr')) ->raw(";\n") ; diff --git a/src/Node/SandboxNode.php b/src/Node/SandboxNode.php index 0ffef6dbb94..80aecbdba36 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 @@ * * @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 7dea5002310..6b4c873e1ab 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 @@ * * @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 3e417dad60d..fae65fb2cb4 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 @@ * * @author Fabien Potencier */ +#[YieldReady] class TextNode extends Node implements NodeOutputInterface { public function __construct(string $data, int $lineno) @@ -30,17 +32,8 @@ public function compile(Compiler $compiler): void { $compiler->addDebugInfo($this); - if ($compiler->getEnvironment()->useYield()) { - $compiler->write('yield '); - } else { - $compiler - ->checkForOutput(false) - ->write('echo ') - ->checkForOutput(true) - ; - } - $compiler + ->write('yield ') ->string($this->getAttribute('data')) ->raw(";\n") ; diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index 3dd2b07ec8a..9b8c5788466 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 @@ * * @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/NodeVisitor/YieldNotReadyNodeVisitor.php b/src/NodeVisitor/YieldNotReadyNodeVisitor.php new file mode 100644 index 00000000000..34c3ac18ba6 --- /dev/null +++ b/src/NodeVisitor/YieldNotReadyNodeVisitor.php @@ -0,0 +1,60 @@ +useYield = $useYield; + } + + public function enterNode(Node $node, Environment $env): Node + { + $class = \get_class($node); + + if ($node instanceof AbstractExpression || isset($this->yieldReadyNodes[$class])) { + return $node; + } + + if (!$this->yieldReadyNodes[$class] = (bool) (new \ReflectionClass($class))->getAttributes(YieldReady::class)) { + if ($this->useYield) { + throw new \LogicException(sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[YieldReady] attribute.', $class)); + } + + trigger_deprecation('twig/twig', '3.9', 'Twig node "%s" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[YieldReady] attribute.', $class); + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + return $node; + } + + public function getPriority(): int + { + return 255; + } +} diff --git a/src/Resources/core.php b/src/Resources/core.php index f58a1fdf261..eb08cafcd3a 100644 --- a/src/Resources/core.php +++ b/src/Resources/core.php @@ -14,484 +14,484 @@ /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_cycle($values, $position) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::cycle($values, $position); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_random(Environment $env, $values = null, $max = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::random($env, $values, $max); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::dateFormatFilter($env, $date, $format, $timezone); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_date_modify_filter(Environment $env, $date, $modifier) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::dateModifyFilter($env, $date, $modifier); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_sprintf($format, ...$values) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::sprintf($format, ...$values); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_date_converter(Environment $env, $date = null, $timezone = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::dateConverter($env, $date, $timezone); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_replace_filter($str, $from) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::replaceFilter($str, $from); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_round($value, $precision = 0, $method = 'common') { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::round($value, $precision, $method); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::numberFormatFilter($env, $number, $decimal, $decimalPoint, $thousandSep); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_urlencode_filter($url) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::urlencodeFilter($url); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_merge(...$arrays) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayMerge(...$arrays); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::slice($env, $item, $start, $length, $preserveKeys); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_first(Environment $env, $item) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::first($env, $item); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_last(Environment $env, $item) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::last($env, $item); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_join_filter($value, $glue = '', $and = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::joinFilter($value, $glue, $and); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::splitFilter($env, $value, $delimiter, $limit); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_get_array_keys_filter($array) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::getArrayKeysFilter($array); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::reverseFilter($env, $item, $preserveKeys); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_sort_filter(Environment $env, $array, $arrow = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::sortFilter($env, $array, $arrow); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_matches(string $regexp, ?string $str) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::matches($regexp, $str); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_trim_filter($string, $characterMask = null, $side = 'both') { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::trimFilter($string, $characterMask, $side); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_nl2br($string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::nl2br($string); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_spaceless($content) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::spaceless($content); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_convert_encoding($string, $to, $from) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::convertEncoding($string, $to, $from); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_length_filter(Environment $env, $thing) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::lengthFilter($env, $thing); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_upper_filter(Environment $env, $string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::upperFilter($env, $string); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_lower_filter(Environment $env, $string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::lowerFilter($env, $string); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_striptags($string, $allowable_tags = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::striptags($string, $allowable_tags); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_title_string_filter(Environment $env, $string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::titleStringFilter($env, $string); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_capitalize_string_filter(Environment $env, $string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::capitalizeStringFilter($env, $string); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_test_empty($value) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::testEmpty($value); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_test_iterable($value) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return is_iterable($value); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::include($env, $context, $template, $variables, $withContext, $ignoreMissing, $sandboxed); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_source(Environment $env, $name, $ignoreMissing = false) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::source($env, $name, $ignoreMissing); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_constant($constant, $object = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::constant($constant, $object); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_constant_is_defined($constant, $object = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::constantIsDefined($constant, $object); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayBatch($items, $size, $fill, $preserveKeys); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_column($array, $name, $index = null): array { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayColumn($array, $name, $index); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_filter(Environment $env, $array, $arrow) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayFilter($env, $array, $arrow); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_map(Environment $env, $array, $arrow) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayMap($env, $array, $arrow); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayReduce($env, $array, $arrow, $initial); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_some(Environment $env, $array, $arrow) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arraySome($env, $array, $arrow); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_every(Environment $env, $array, $arrow) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayEvery($env, $array, $arrow); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::checkArrowInSandbox($env, $arrow, $thing, $type); } diff --git a/src/Resources/debug.php b/src/Resources/debug.php index 0041b4b79ac..6f59cf6c13d 100644 --- a/src/Resources/debug.php +++ b/src/Resources/debug.php @@ -14,11 +14,11 @@ /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_var_dump(Environment $env, $context, ...$vars) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); DebugExtension::dump($env, $context, ...$vars); } diff --git a/src/Resources/escaper.php b/src/Resources/escaper.php index cf038645f8e..ea377803504 100644 --- a/src/Resources/escaper.php +++ b/src/Resources/escaper.php @@ -14,22 +14,22 @@ /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_raw_filter($string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return $string; } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return EscaperExtension::escape($env, $string, $strategy, $charset, $autoescape); } diff --git a/src/Resources/string_loader.php b/src/Resources/string_loader.php index af5e152b2d7..8f0e6492aab 100644 --- a/src/Resources/string_loader.php +++ b/src/Resources/string_loader.php @@ -16,11 +16,11 @@ /** * @internal * - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_template_from_string(Environment $env, $template, ?string $name = null): TemplateWrapper { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return StringLoaderExtension::templateFromString($env, $template, $name); } diff --git a/src/Template.php b/src/Template.php index 0c06ac9187d..bb8146a2c5b 100644 --- a/src/Template.php +++ b/src/Template.php @@ -41,9 +41,12 @@ abstract class Template protected $extensions = []; protected $sandbox; + private $useYield; + public function __construct(Environment $env) { $this->env = $env; + $this->useYield = $env->useYield(); $this->extensions = $env->getExtensions(); } @@ -74,7 +77,7 @@ abstract public function getSourceContext(); * 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) { @@ -83,9 +86,7 @@ public function getParent(array $context) } try { - $parent = $this->doGetParent($context); - - if (false === $parent) { + if (!$parent = $this->doGetParent($context)) { return false; } @@ -128,12 +129,8 @@ public function isTraitable() */ public function displayParentBlock($name, array $context, array $blocks = []) { - if (isset($this->traits[$name])) { - $this->traits[$name][0]->displayBlock($name, $context, $blocks, false); - } elseif (false !== $parent = $this->getParent($context)) { - $parent->displayBlock($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()); + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + echo $data; } } @@ -150,49 +147,8 @@ public function displayParentBlock($name, array $context, array $blocks = []) */ public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $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 self) { - throw new \LogicException('A block must be a method on a \Twig\Template instance.'); - } - - if (null !== $template) { - try { - $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)) { - $parent->displayBlock($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()); + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) { + echo $data; } } @@ -210,14 +166,12 @@ public function displayBlock($name, array $context, array $blocks = [], $useBloc */ public function renderParentBlock($name, array $context, array $blocks = []) { - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); + $content = ''; + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + $content .= $data; } - $this->displayParentBlock($name, $context, $blocks); - return ob_get_clean(); + return $content; } /** @@ -235,23 +189,12 @@ public function renderParentBlock($name, array $context, array $blocks = []) */ public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true) { - $level = ob_get_level(); - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); + $content = ''; + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) { + $content .= $data; } - try { - $this->displayBlock($name, $context, $blocks, $useBlocks); - } catch (\Throwable $e) { - while (ob_get_level() > $level) { - ob_end_clean(); - } - throw $e; - } - - return ob_get_clean(); + return $content; } /** @@ -276,7 +219,7 @@ public function hasBlock($name, array $context, array $blocks = []) return true; } - if (false !== $parent = $this->getParent($context)) { + if ($parent = $this->getParent($context)) { return $parent->hasBlock($name, $context); } @@ -298,7 +241,7 @@ public function getBlockNames(array $context, array $blocks = []) { $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)); } @@ -306,7 +249,7 @@ public function getBlockNames(array $context, array $blocks = []) } /** - * @return Template|TemplateWrapper + * @return self|TemplateWrapper */ protected function loadTemplate($template, $templateName = null, $line = null, $index = null) { @@ -351,7 +294,7 @@ protected function loadTemplate($template, $templateName = null, $line = null, $ /** * @internal * - * @return Template + * @return self */ public function unwrap() { @@ -371,36 +314,52 @@ public function getBlocks() return $this->blocks; } - public function display(array $context, array $blocks = []) + public function display(array $context, array $blocks = []): void { - $this->displayWithErrorHandling($this->env->mergeGlobals($context), array_merge($this->blocks, $blocks)); + foreach ($this->yield($context, $blocks) as $data) { + echo $data; + } } - public function render(array $context) + public function render(array $context): string { - $level = ob_get_level(); - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); + $content = ''; + foreach ($this->yield($context) as $data) { + $content .= $data; } - try { - $this->display($context); - } catch (\Throwable $e) { - while (ob_get_level() > $level) { - ob_end_clean(); - } - throw $e; - } - - return ob_get_clean(); + return $content; } - protected function displayWithErrorHandling(array $context, array $blocks = []) + /** + * @return iterable + */ + public function yield(array $context, array $blocks = []): iterable { + $context = $this->env->mergeGlobals($context); + $blocks = array_merge($this->blocks, $blocks); + try { - $this->doDisplay($context, $blocks); + if ($this->useYield) { + yield from $this->doDisplay($context, $blocks); + return; + } + + $level = ob_get_level(); + ob_start(); + + foreach ($this->doDisplay($context, $blocks) as $data) { + if (ob_get_length()) { + $data = ob_get_clean().$data; + ob_start(); + } + + yield $data; + } + + if (ob_get_length()) { + yield ob_get_clean(); + } } catch (Error $e) { if (!$e->getSourceContext()) { $e->setSourceContext($this->getSourceContext()); @@ -418,6 +377,111 @@ protected function displayWithErrorHandling(array $context, array $blocks = []) $e->guess(); throw $e; + } finally { + if (!$this->useYield) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + } + } + } + + /** + * @return iterable + */ + public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $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 self) { + throw new \LogicException('A block must be a method on a \Twig\Template instance.'); + } + + if (null !== $template) { + try { + if ($this->useYield) { + yield from $template->$block($context, $blocks); + return; + } + + $level = ob_get_level(); + ob_start(); + + foreach ($template->$block($context, $blocks) as $data) { + if (ob_get_length()) { + $data = ob_get_clean().$data; + ob_start(); + } + + yield $data; + } + + if (ob_get_length()) { + yield ob_get_clean(); + } + } 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; + } finally { + if (!$this->useYield) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + } + } + } 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()); + } else { + throw new RuntimeError(sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); + } + } + + /** + * 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 + * + * @return iterable + */ + 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 ($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()); } } diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index f20a1cf9641..fcfb070c799 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -66,12 +66,8 @@ public function renderBlock(string $name, array $context = []): string public function displayBlock(string $name, array $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); + foreach ($this->template->yieldBlock($name, $context) as $data) { + echo $data; } } diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index b4ddafe7598..30d6810f8af 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -67,19 +67,4 @@ protected function getAttributeGetter() { return 'CoreExtension::getAttribute($this->env, $this->source, '; } - - protected function getEchoOrYield(): string - { - return ($this->currentEnv ?? $this->getEnvironment())->useYield() ? 'yield' : 'echo'; - } - - protected function getDisplayOrYield(string $expr): string - { - return sprintf(($this->currentEnv ?? $this->getEnvironment())->useYield() ? 'yield from %s->unwrap()->yield' : '%s->display', $expr); - } - - protected function getDisplayOrYieldBlock(string $expr): string - { - return sprintf(($this->currentEnv ?? $this->getEnvironment())->useYield() ? 'yield from %s->unwrap()->yieldBlock' : '%s->displayBlock', $expr); - } } diff --git a/src/YieldingTemplate.php b/src/YieldingTemplate.php deleted file mode 100644 index c9d7a578b44..00000000000 --- a/src/YieldingTemplate.php +++ /dev/null @@ -1,182 +0,0 @@ - - * - * @internal - */ -abstract class YieldingTemplate extends Template -{ - /** - * @return iterable - */ - 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($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) - { - 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 - * - * @return iterable - */ - 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 renderParentBlock($name, array $context, array $blocks = []) - { - $content = ''; - foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { - $content .= $data; - } - - return $content; - } - - 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()); - } - - 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()); - } -} diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index b22fdf42450..688024b8e02 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -12,10 +12,12 @@ */ use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; 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; @@ -34,6 +36,8 @@ class EnvironmentTest extends TestCase { + use ExpectDeprecationTrait; + public function testAutoescapeOption() { $loader = new ArrayLoader([ @@ -403,6 +407,32 @@ public function testUndefinedTokenParserCallback() $this->assertSame('dynamic', $parser->getTag()); } + /** + * @group legacy + * + * @requires PHP 8 + */ + 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()); + + if ($twig->useYield()) { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('An exception has been thrown during the compilation of a template ("You cannot enable the "use_yield" option of Twig as node "Twig\Tests\EnvironmentTest_LegacyEchoingNode" is not marked as ready for it; please make it ready and then flag it with the #[YieldReady] attribute.") in "echo_bar".'); + } else { + $this->expectDeprecation(<<<'EOF' +Since twig/twig 3.9: Twig node "Twig\Tests\EnvironmentTest_LegacyEchoingNode" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[YieldReady] attribute. + Since twig/twig 3.9: Using "echo" is deprecated, use "yield" instead in "Twig\Tests\EnvironmentTest_LegacyEchoingNode", then flag the class with #[YieldReady]. +EOF + ); + } + + $this->assertSame('ADbarEBbarCF', $twig->render('echo_bar')); + } + protected function getMockLoader($templateName, $templateContent) { $loader = $this->createMock(LoaderInterface::class); @@ -486,6 +516,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 +563,14 @@ public function fromRuntime($name = 'bar') 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 b2df9b1605c..9cf18742bb7 100644 --- a/tests/Node/AutoEscapeTest.php +++ b/tests/Node/AutoEscapeTest.php @@ -31,10 +31,9 @@ public function getTests() { $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 f291f29f33c..1211ee17b83 100644 --- a/tests/Node/BlockReferenceTest.php +++ b/tests/Node/BlockReferenceTest.php @@ -26,9 +26,9 @@ public function testConstructor() 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 a021145667d..280a95cba4a 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -32,41 +32,17 @@ public function testConstructor() public function getTests() { $tests = []; - - if (!$this->getEnvironment()->useYield()) { - $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), <<macros; - echo "foo"; -} -EOF - , new Environment(new ArrayLoader()), - ]; - } else { - $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), <<macros; yield "foo"; + return; yield ''; } EOF - , new Environment(new ArrayLoader()), - ]; - - $tests[] = [new BlockNode('foo', new Node(), 1), <<macros; - 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 5b6a3ee4f51..3ca4b22a304 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -53,14 +53,13 @@ public function getTests() $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 @@ public function getTests() \$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 @@ public function getTests() \$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 @@ public function getTests() \$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 @@ public function getTests() } } 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 5dda061d0cb..26821a39b35 100644 --- a/tests/Node/IfTest.php +++ b/tests/Node/IfTest.php @@ -47,12 +47,11 @@ public function getTests() ], [], 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 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; } elseif (false) { - $displayStmt {$this->getVariableGetter('bar')}; + yield {$this->getVariableGetter('bar')}; } EOF ]; @@ -86,9 +85,9 @@ public function getTests() $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 ee68339c5d3..cda9d7bf27f 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -40,9 +40,9 @@ public function getTests() $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 @@ public function getTests() 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 @@ public function getTests() // 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 948949af66d..09d7ee6ca7a 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -45,8 +45,7 @@ public function getTests() $body = new TextNode('foo', 1); $node = new MacroNode('foo', $body, $arguments, 1); - if ($this->getEnvironment()->useYield()) { - $text[] = [$node, <<env->getCharset()); } EOF - , new Environment(new ArrayLoader()), - ]; - } else { - $body = new TextNode('foo', 1); - $node = new MacroNode('foo', $body, $arguments, 1); - - $tests[] = [$node, <<macros; - \$context = \$this->env->mergeGlobals([ - "foo" => \$__foo__, - "bar" => \$__bar__, - "varargs" => \$__varargs__, - ]); - - \$blocks = []; - - 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(); - } - })(); -} -EOF - , new Environment(new ArrayLoader()), - ]; - } + , new Environment(new ArrayLoader()), + ]; return $tests; } diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 240b69e83ac..2b5e79c2c8e 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -55,8 +55,6 @@ public function getTests() $macros = new Node(); $traits = new Node(); $source = new Source('{{ foo }}', 'foo.twig'); - $parentTemplate = $this->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template'; - $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 ''; } /** @@ -128,7 +127,6 @@ public function getSourceContext() $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, <<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)); } /** @@ -219,7 +217,6 @@ public function getSourceContext() 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); @@ -237,10 +234,10 @@ public function getSourceContext() use Twig\Sandbox\SecurityNotAllowedFilterError; use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; -use Twig\\{$parentTemplate}; +use Twig\Template; /* foo.twig */ -class __TwigTemplate_%x extends $parentTemplate +class __TwigTemplate_%x extends Template { private \$source; private \$macros = []; @@ -267,7 +264,7 @@ protected function doDisplay(array \$context, array \$blocks = []) // 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 f951c2e3695..2df440c2808 100644 --- a/tests/Node/PrintTest.php +++ b/tests/Node/PrintTest.php @@ -28,9 +28,7 @@ public function testConstructor() 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 bf16f1f03c0..c74feba42f3 100644 --- a/tests/Node/SandboxTest.php +++ b/tests/Node/SandboxTest.php @@ -31,7 +31,6 @@ public function getTests() $body = new TextNode('foo', 1); $node = new SandboxNode($body, 1); - $displayStmt = $this->getEchoOrYield(); $tests[] = [$node, <<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 81ba3ea4629..57f66cb9d61 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -62,18 +62,11 @@ public function getTests() , new Environment(new ArrayLoader()), ]; } else { - $tests[] = [$node, <<env->getCharset()); - } finally { - ob_end_clean(); - } -})(); +$context["foo"] = ('' === $tmp = \Twig\Extension\CoreExtension::captureOutput((function () use (&$context, $macros, $blocks) { + yield "foo"; +})() ?? new \EmptyIterator())) ? '' : new Markup($tmp, $this->env->getCharset()); EOF , new Environment(new ArrayLoader()), ]; @@ -91,9 +84,9 @@ public function getTests() $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 31639cc2deb..357362c3c7e 100644 --- a/tests/Node/TextTest.php +++ b/tests/Node/TextTest.php @@ -26,8 +26,7 @@ public function testConstructor() 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 776ac3fa806..a302aba059d 100644 --- a/tests/TemplateWrapperTest.php +++ b/tests/TemplateWrapperTest.php @@ -60,17 +60,13 @@ public function testDisplayBlock() 'index' => '{% block foo %}{{ foo }}{{ bar }}{% endblock %}', ])); - if (!$twig->useYield()) { - $twig->addGlobal('bar', 'BAR'); + $twig->addGlobal('bar', 'BAR'); - $wrapper = $twig->load('index'); + $wrapper = $twig->load('index'); - ob_start(); - $wrapper->displayBlock('foo', ['foo' => 'FOO']); + ob_start(); + $wrapper->displayBlock('foo', ['foo' => 'FOO']); - $this->assertEquals('FOOBAR', ob_get_clean()); - } else { - $this->markTestSkipped('yield not used.'); - } + $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 0f844211547..00000000000 --- 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.%