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 %}']);