From c53b6468c9e6c2fc0d6bdd553c518b8bd3cae39d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 11 Sep 2026 06:10:11 -0700 Subject: [PATCH] Resolve constant parent templates once instead of on every lookup --- CHANGELOG | 1 + src/Node/ModuleNode.php | 19 ++++++++++--------- tests/Node/ModuleTest.php | 2 +- tests/TemplateWrapperTest.php | 13 +++++++++++++ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7b222909e..b0fb15187 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ * Fix `TemplateWrapper::streamBlock()`, `TemplateWrapper::hasBlock()`, and `TemplateWrapper::getBlockNames()` omitting environment globals * Fix exceptions from dynamic parent expressions escaping without template context * Add the `BlockChain` class to compose blocks from multiple templates without using template internals + * Fix `TemplateWrapper::hasBlock()` and `TemplateWrapper::getBlockNames()` losing the `extends` line when the parent template does not exist * Fix an output buffer leak when a parent block rendered in an expression throws in non-yield mode * Add the `HtmlExtension::htmlAttrValue()` method to resolve a single HTML attribute value the way the `html_attr` function renders it * Fix `html_attr` JSON encoding a `Stringable` value in a `data-*` attribute instead of using its string representation diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index ca2b0b9c2..13eef99ad 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -143,17 +143,18 @@ final class ModuleNode extends Node implements CoercesChildrenToStringInterface ; if ($parent instanceof ConstantExpression) { - $compiler->subcompile($parent); - } else { - $compiler - ->raw('$this->load(') - ->subcompile($parent) - ->raw(', ') - ->repr($parent->getTemplateLine()) - ->raw(')') - ; + // a constant parent never depends on the context, so resolve it once + $compiler->raw('$this->parent ??= '); } + $compiler + ->raw('$this->load(') + ->subcompile($parent) + ->raw(', ') + ->repr($parent->getTemplateLine()) + ->raw(')') + ; + $compiler ->raw(";\n") ->outdent() diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index c3cbad02d..670309367 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -232,7 +232,7 @@ class __TwigTemplate_%x extends Template protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper { // line 1 - return "layout.twig"; + return \$this->parent ??= \$this->load("layout.twig", 1); } protected function doDisplay(array \$context, array \$blocks = []): iterable diff --git a/tests/TemplateWrapperTest.php b/tests/TemplateWrapperTest.php index b5a8b6be3..197e3aa13 100644 --- a/tests/TemplateWrapperTest.php +++ b/tests/TemplateWrapperTest.php @@ -139,6 +139,19 @@ class TemplateWrapperTest extends TestCase yield 'exception thrown by parent expression' => ['{% extends boom() %}', new TwigFunction('boom', static function (): never { throw new \DomainException('kaboom'); }), \DomainException::class]; } + public function testBlockIntrospectionReportsTheExtendsLineForAMissingParent(): void + { + $twig = new Environment(new ArrayLoader(['index' => "\n\n{% extends 'missing' %}"])); + + try { + $twig->load('index')->hasBlock('foo'); + $this->fail('Introspecting a template with a missing parent must fail.'); + } catch (LoaderError $e) { + $this->assertSame('Template "missing" is not defined.', $e->getRawMessage()); + $this->assertSame(3, $e->getTemplateLine()); + } + } + public function testRenderBlock(): void { $twig = new Environment(new ArrayLoader([