From b73ab8cfd9821ac6e99c2a2abc796d4ae608beed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 30 Apr 2026 13:33:43 +0200 Subject: [PATCH] Lazy load EscaperRuntime in EscaperExtension Previously, setEnvironment() called getRuntime(EscaperRuntime::class) eagerly, which prevented overriding EscaperRuntime via a custom runtime loader since Environment::__construct() calls setEnvironment() before any loader can be injected. --- src/Extension/EscaperExtension.php | 29 ++++++++++++++++------------- tests/Extension/EscaperTest.php | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index c5625fa6a..6d9d1ce24 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -76,7 +76,7 @@ final class EscaperExtension extends AbstractExtension } $this->environment = $environment; - $this->escaper = $environment->getRuntime(EscaperRuntime::class); + $this->escaper = null; } /** @@ -140,16 +140,14 @@ final class EscaperExtension extends AbstractExtension { trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__); - if (!isset($this->environment)) { - throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); - } + $escaper = $this->getEscaper(__METHOD__); $this->escapers[$strategy] = $callable; $callable = function ($string, $charset) use ($callable) { return $callable($this->environment, $string, $charset); }; - $this->escaper->setEscaper($strategy, $callable); + $escaper->setEscaper($strategy, $callable); } /** @@ -175,11 +173,7 @@ final class EscaperExtension extends AbstractExtension { trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__); - if (!isset($this->escaper)) { - throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); - } - - $this->escaper->setSafeClasses($safeClasses); + $this->getEscaper(__METHOD__)->setSafeClasses($safeClasses); } /** @@ -191,11 +185,20 @@ final class EscaperExtension extends AbstractExtension { trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__); - if (!isset($this->escaper)) { - throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); + $this->getEscaper(__METHOD__)->addSafeClass($class, $strategies); + } + + private function getEscaper(string $fromMethod): EscaperRuntime + { + if (isset($this->escaper)) { + return $this->escaper; } - $this->escaper->addSafeClass($class, $strategies); + if (isset($this->environment)) { + return $this->escaper = $this->environment->getRuntime(EscaperRuntime::class); + } + + throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', $fromMethod)); } /** diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index 52f06950c..09aa4ca79 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -25,6 +25,7 @@ use Twig\Environment; use Twig\Extension\EscaperExtension; use Twig\Loader\ArrayLoader; use Twig\Runtime\EscaperRuntime; +use Twig\RuntimeLoader\FactoryRuntimeLoader; class EscaperTest extends TestCase { @@ -80,6 +81,24 @@ class EscaperTest extends TestCase $this->assertSame('foo**ISO-8859-1**UTF-8**again', $env2->getRuntime(EscaperRuntime::class)->escape('foo', 'foo', 'ISO-8859-1')); } + /** + * @group legacy + */ + public function testSafeClassesUseCustomRuntimeLoader() + { + $runtime = new EscaperRuntime(); + $twig = new Environment(new ArrayLoader()); + $twig->addRuntimeLoader(new FactoryRuntimeLoader([ + EscaperRuntime::class => static fn () => $runtime, + ])); + + $escaperExt = $twig->getExtension(EscaperExtension::class); + $escaperExt->addSafeClass('ThisClassIsSafe', ['html']); + + $this->assertSame($runtime, $twig->getRuntime(EscaperRuntime::class)); + $this->assertArrayHasKey('ThisClassIsSafe', $runtime->safeClasses); + } + public function testLastModified() { $this->assertGreaterThan(1000000000, (new EscaperExtension())->getLastModified());