From 72c2f669bd604abb677736fce7fccb1cec55cf97 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 12 Sep 2026 12:17:17 +0200 Subject: [PATCH 1/2] Report a clear error when using macros imported in a template body that was not rendered --- CHANGELOG | 1 + .../Expression/Variable/MacroVariable.php | 6 ++-- src/Template.php | 8 +++++ tests/BlockChainTest.php | 3 +- tests/CallMacroTest.php | 36 +++++++++++++++++++ 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b0fb15187..75589bb89 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.29.0 (2026-XX-XX) + * Fix the PHP warning and cryptic error when a block or a macro rendered on its own uses macros imported in the template body * Fix `{% cache %}` always missing in the Symfony bundle when `framework.cache.app` uses a natively tag aware adapter * Fix the sandbox resolving `use` trait templates before checking that the `use` tag is allowed * Fix `html_attr` dropping `style` declarations whose value is `0`, `0.0` or `'0'` diff --git a/src/Node/Expression/Variable/MacroVariable.php b/src/Node/Expression/Variable/MacroVariable.php index 032328120..c5ea1ae57 100644 --- a/src/Node/Expression/Variable/MacroVariable.php +++ b/src/Node/Expression/Variable/MacroVariable.php @@ -33,9 +33,11 @@ class MacroVariable extends TempNameExpression $compiler->raw('$this->getMacroNamespace()'); } else { $compiler - ->raw('$macros[') + ->raw('($macros[') ->string($name) - ->raw(']') + ->raw('] ?? $this->throwUninitializedMacroNamespace(') + ->repr($this->getTemplateLine()) + ->raw('))') ; } } diff --git a/src/Template.php b/src/Template.php index a3023489d..b81bb9033 100644 --- a/src/Template.php +++ b/src/Template.php @@ -559,6 +559,14 @@ abstract class Template { } + /** + * @internal + */ + protected function throwUninitializedMacroNamespace(int $line): never + { + throw new RuntimeError(\sprintf('Macros imported in the body of template "%s" are not available because the body was not rendered; move the "import" or "from" tag inside the block or the macro that uses it.', $this->getTemplateName()), $line, $this->getSourceContext()); + } + /** * Auto-generated method to display the template with the given context. * diff --git a/tests/BlockChainTest.php b/tests/BlockChainTest.php index f096575fa..4252bed3c 100644 --- a/tests/BlockChainTest.php +++ b/tests/BlockChainTest.php @@ -480,7 +480,8 @@ class BlockChainTest extends TestCase try { $chain->renderBlock('field', ['helper' => 'macros1']); $this->fail('Rendering an uninitialized import must fail.'); - } catch (RuntimeError) { + } catch (RuntimeError $e) { + $this->assertStringContainsString('Macros imported in the body of template "theme" are not available because the body was not rendered', $e->getMessage()); } $this->assertSame('one', $twig->render('theme', ['helper' => 'macros1'])); diff --git a/tests/CallMacroTest.php b/tests/CallMacroTest.php index 3b37daf91..9bf1b600e 100644 --- a/tests/CallMacroTest.php +++ b/tests/CallMacroTest.php @@ -197,6 +197,42 @@ class CallMacroTest extends TestCase $template->getMacroNamespace()->call('missing', [], [], 1, new Source('', 'index')); } + public function testRenderingABlockOnItsOwnReportsMacrosImportedInTheTemplateBody(): void + { + $template = $this->load([ + 'index' => '{% import "macros" as helpers %}{% block field %}{{ helpers.label() }}{% endblock %}', + 'macros' => '{% macro label() %}label{% endmacro %}', + ]); + + $this->expectException(RuntimeError::class); + $this->expectExceptionMessage('Macros imported in the body of template "index" are not available because the body was not rendered; move the "import" or "from" tag inside the block or the macro that uses it in "index" at line 1.'); + + $template->renderBlock('field', []); + } + + public function testRenderingABlockOnItsOwnUsesMacrosImportedInTheBlock(): void + { + $template = $this->load([ + 'index' => '{% block field %}{% import "macros" as helpers %}{{ helpers.label() }}{% endblock %}', + 'macros' => '{% macro label() %}label{% endmacro %}', + ]); + + $this->assertSame('label', $template->renderBlock('field', [])); + } + + public function testCallingAMacroOnItsOwnReportsMacrosImportedInTheTemplateBody(): void + { + $template = $this->load([ + 'index' => '{% from "macros" import label %}{% macro row() %}{{ label() }}{% endmacro %}', + 'macros' => '{% macro label() %}label{% endmacro %}', + ]); + + $this->expectException(RuntimeError::class); + $this->expectExceptionMessage('Macros imported in the body of template "index" are not available because the body was not rendered'); + + $this->callMacro($template, 'row', []); + } + public function testDeprecatedCoreExtensionCallMacroAcceptsMacroNamespace(): void { $template = $this->load(['index' => '{% macro greet(name) %}Hi {{ name }}{% endmacro %}']); From 56e5c0794b9ace8414c4e525221b5b203d65cf10 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 14 Sep 2026 11:59:20 +0200 Subject: [PATCH 2/2] Clarify the exception message for nested block chains from another environment --- src/BlockChain.php | 2 +- tests/BlockChainTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BlockChain.php b/src/BlockChain.php index 81d0a7105..64fa8652d 100644 --- a/src/BlockChain.php +++ b/src/BlockChain.php @@ -44,7 +44,7 @@ final class BlockChain foreach ($templates as $template) { if ($template instanceof self) { if ($env !== $template->env) { - throw new \LogicException('A block chain cannot contain templates from different Twig environments.'); + throw new \LogicException('A block chain cannot contain a block chain from a different Twig environment.'); } $this->templates[] = $template; diff --git a/tests/BlockChainTest.php b/tests/BlockChainTest.php index 7d7b6e2d1..a01c6184f 100644 --- a/tests/BlockChainTest.php +++ b/tests/BlockChainTest.php @@ -335,7 +335,7 @@ class BlockChainTest extends TestCase $other = new Environment(new ArrayLoader(['theme' => ''])); $this->expectException(\LogicException::class); - $this->expectExceptionMessage('A block chain cannot contain templates from different Twig environments.'); + $this->expectExceptionMessage('A block chain cannot contain a block chain from a different Twig environment.'); new BlockChain($twig, [new BlockChain($other, ['theme'])]); }