Wrap dynamic parent expression errors

This commit is contained in:
Fabien Potencier
2026-09-06 17:14:45 +02:00
parent 32acc4c3b9
commit b6da5e67f3
3 changed files with 58 additions and 1 deletions
+1
View File
@@ -4,6 +4,7 @@
* Fix the `default` filter fallback emitting an undefined variable warning when it uses the null-safe operator
* Fix the `matches` operator silently treating PCRE execution errors as non-matches
* Fix `TemplateWrapper::streamBlock()`, `TemplateWrapper::hasBlock()`, and `TemplateWrapper::getBlockNames()` omitting environment globals
* Fix exceptions from dynamic parent expressions escaping without template context
* 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
* Add documentation comments to attach metadata to nodes (experimental)
+7 -1
View File
@@ -89,7 +89,13 @@ abstract class Template
// resolved against a parent, or yieldBlock() into a pre-warmed instance).
$this->ensureSecurityChecked();
if (!$parent = $this->doGetParent($context)) {
try {
$parent = $this->doGetParent($context);
} catch (\Throwable $e) {
$this->handleException($e);
}
if (!$parent) {
return false;
}
+50
View File
@@ -20,9 +20,13 @@ namespace Twig\Tests;
* file that was distributed with this source code.
*/
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Loader\ArrayLoader;
use Twig\TwigFunction;
class TemplateWrapperTest extends TestCase
{
@@ -89,6 +93,52 @@ class TemplateWrapperTest extends TestCase
$this->assertSame('FOOBAR', $streamed);
}
/**
* @dataProvider provideDynamicParentFailures
*/
#[DataProvider('provideDynamicParentFailures')]
public function testDynamicParentFailuresAreWrapped(string $template, ?TwigFunction $function, string $previousException): void
{
$twig = new Environment(new ArrayLoader(['index' => $template]));
if (null !== $function) {
$twig->addFunction($function);
}
foreach ([
'block introspection' => static fn () => $twig->load('index')->getBlockNames(),
'rendering' => static fn () => $twig->render('index'),
] as $entryPoint => $call) {
try {
$call();
$this->fail(\sprintf('Resolving the dynamic parent during %s must fail.', $entryPoint));
} catch (RuntimeError $e) {
$this->assertSame('index', $e->getSourceContext()->getName());
$this->assertSame(1, $e->getTemplateLine());
$this->assertInstanceOf($previousException, $e->getPrevious());
}
}
}
public function testDynamicParentTwigErrorsAreNotWrapped(): void
{
$twig = new Environment(new ArrayLoader(['index' => '{% extends parent %}']));
try {
$twig->load('index')->getBlockNames(['parent' => 'missing']);
$this->fail('Loading the dynamic parent must fail.');
} catch (LoaderError $e) {
$this->assertSame('index', $e->getSourceContext()->getName());
$this->assertSame(1, $e->getTemplateLine());
$this->assertNull($e->getPrevious());
}
}
public static function provideDynamicParentFailures(): iterable
{
yield 'missing parent variable' => ['{% extends parent %}', null, \TypeError::class];
yield 'exception thrown by parent expression' => ['{% extends boom() %}', new TwigFunction('boom', static function (): never { throw new \DomainException('kaboom'); }), \DomainException::class];
}
public function testRenderBlock(): void
{
$twig = new Environment(new ArrayLoader([