Report a clear error when using macros imported in a template body that was not rendered

This commit is contained in:
Fabien Potencier
2026-09-12 12:17:17 +02:00
parent a414c3a491
commit 72c2f669bd
5 changed files with 51 additions and 3 deletions
+1
View File
@@ -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'`
@@ -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('))')
;
}
}
+8
View File
@@ -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.
*
+2 -1
View File
@@ -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']));
+36
View File
@@ -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 %}']);