Merge branch '3.x' into 4.x

* 3.x:
  Clarify the exception message for nested block chains from another environment
  Report a clear error when using macros imported in a template body that was not rendered

# Conflicts:
#	CHANGELOG
#	src/Template.php
#	tests/CallMacroTest.php
This commit is contained in:
Fabien Potencier
2026-09-14 14:07:14 +02:00
5 changed files with 52 additions and 5 deletions
+1 -1
View File
@@ -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;
@@ -52,9 +52,11 @@ class MacroVariable extends AbstractExpression
$compiler->raw('$this->getMacroNamespace()');
} else {
$compiler
->raw('$macros[')
->raw('($macros[')
->string($name)
->raw(']')
->raw('] ?? $this->throwUninitializedMacroNamespace(')
->repr($this->getTemplateLine())
->raw('))')
;
}
}
+8
View File
@@ -486,6 +486,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());
}
private function handleException(\Throwable $error): never
{
if ($error instanceof Error) {
+3 -2
View File
@@ -296,7 +296,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'])]);
}
@@ -519,7 +519,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
@@ -134,6 +134,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', []);
}
private function callMacro(Template $template, string $name, array $arguments): mixed
{
return $template->getMacroNamespace()->call($name, $arguments, [], 1, new Source('', 'index'));