Repository: twigphp/Twig (php)

Issue:
 call setEscaperRuntime() before calling Twig\Extension\EscaperExtension::addSafeClass().  
I just started getting this error, using Symfony ^7.0 for all the bundles.


```
 bin/console c:c -v

In EscaperExtension.php line 177:
                                                                                                     
  [LogicException]                                                                                   
  You must call setEscaperRuntime() before calling Twig\Extension\EscaperExtension::addSafeClass().  
                                                                                                     
```

```
Exception trace:
  at /home/tac/g/sites/packages/vendor/twig/twig/src/Extension/EscaperExtension.php:177
 Twig\Extension\EscaperExtension->addSafeClass() at /home/tac/g/sites/packages/vendor/symfony/ux-twig-component/src/Twig/TwigEnvironmentConfigurator.php:35
 Symfony\UX\TwigComponent\Twig\TwigEnvironmentConfigurator->configure() at /home/tac/g/sites/packages/var/cache/test/ContainerBaHlBOQ/App_KernelTestDebugContainer.php:5596
 ContainerBaHlBOQ\App_KernelTestDebugContainer::getTwigService() at /home/tac/g/sites/packages/var/cache/test/ContainerBaHlBOQ/App_KernelTestDebugContainer.php:3737
 ContainerBaHlBOQ\App_KernelTestDebugContainer::getDataCollector_TwigService() at /home/tac/g/sites/packages/var/cache/test/ContainerBaHlBOQ/App_KernelTestDebugContainer.php:762
 ContainerBaHlBOQ\App_KernelTestDebugContainer::get_Container_Private_ProfilerService() at /home/tac/g/sites/packages/var/cache/test/ContainerBaHlBOQ/getConsoleProfilerListenerService.php:23
 ContainerBaHlBOQ\getConsoleProfilerListenerService::do() at /home/tac/g/sites/packages/var/cache/test/ContainerBaHlBOQ/App_KernelTestDebugContainer.php:728
 ContainerBaHlBOQ\App_KernelTestDebugContainer->load() at /home/tac/g/sites/packages/var/cache/test/ContainerBaHlBOQ/App_KernelTestDebugContainer.php:999
 ContainerBaHlBOQ\App_KernelTestDebugContainer::ContainerBaHlBOQ\{closure}() at /home/tac/g/sites/packages/vendor/symfony/event-dispatcher/EventDispatcher.php:221
 Symfony\Component\EventDispatcher\EventDispatcher->sortListeners() at /home/tac/g/sites/packages/vendor/symfony/event-dispatcher/EventDispatcher.php:70
 Symfony\Component\EventDispatcher\EventDispatcher->getListeners() at /home/tac/g/sites/packages/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:257
 Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->preProcess() at /home/tac/g/sites/packages/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:121
 Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch() at /home/tac/g/sites/packages/vendor/symfony/console/Application.php:1046
 Symfony\Component\Console\Application->doRunCommand() at /home/tac/g/sites/packages/vendor/symfony/framework-bundle/Console/Application.php:125
 Symfony\Bundle\FrameworkBundle\Console\Application->doRunCommand() at /home/tac/g/sites/packages/vendor/symfony/console/Application.php:318
 Symfony\Component\Console\Application->doRun() at /home/tac/g/sites/packages/vendor/symfony/framework-bundle/Console/Application.php:79
 Symfony\Bundle\FrameworkBundle\Console\Application->doRun() at /home/tac/g/sites/packages/vendor/symfony/console/Application.php:169
 Symfony\Component\Console\Application->run() at /home/tac/g/sites/packages/vendor/symfony/runtime/Runner/Symfony/ConsoleApplicationRunner.php:49
 Symfony\Component\Runtime\Runner\Symfony\ConsoleApplicationRunner->run() at /home/tac/g/sites/packages/vendor/autoload_runtime.php:29
 require_once() at /home/tac/g/sites/packages/bin/console:11



Pull request:
Fix BC break on escaper extension
Closes #4082

@tacman Can you test that this fixes your issue?

Patch:
diff --git a/CHANGELOG b/CHANGELOG
index a3958294..e8564b90 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,6 @@
 # 3.10.1 (2024-XX-XX)
 
+ * Fix BC break on escaper extension
  * Fix constant return type
 
 # 3.10.0 (2024-05-11)
diff --git a/src/Environment.php b/src/Environment.php
index 10d2cdd9..cfdfb44f 100644
--- a/src/Environment.php
+++ b/src/Environment.php
@@ -135,7 +135,9 @@ class Environment
         ]);
 
         $this->addExtension(new CoreExtension());
-        $this->addExtension(new EscaperExtension($options['autoescape']));
+        $escaperExt = new EscaperExtension($options['autoescape']);
+        $escaperExt->setEnvironment($this, false);
+        $this->addExtension($escaperExt);
         if (\PHP_VERSION_ID >= 80000) {
             $this->addExtension(new YieldNotReadyExtension($this->useYield));
         }
diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php
index a0654cd8..cf821c6d 100644
--- a/src/Extension/EscaperExtension.php
+++ b/src/Extension/EscaperExtension.php
@@ -59,11 +59,14 @@ final class EscaperExtension extends AbstractExtension
     /**
      * @deprecated since Twig 3.10
      */
-    public function setEnvironment(Environment $environment): void
+    public function setEnvironment(Environment $environment, bool $triggerDeprecation = true): void
     {
-        trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__);
+        if ($triggerDeprecation) {
+            trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__);
+        }
 
         $this->environment = $environment;
+        $this->escaper = $environment->getRuntime(EscaperRuntime::class);
     }
 
     /**
@@ -126,9 +129,6 @@ final class EscaperExtension extends AbstractExtension
         if (!isset($this->environment)) {
             throw new \LogicException(sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
         }
-        if (!isset($this->escaper)) {
-            throw new \LogicException(sprintf('You must call "setEscaperRuntime()" before calling "%s()".', __METHOD__));
-        }
 
         $this->escapers[$strategy] = $callable;
         $callable = function ($string, $charset) use ($callable) {
@@ -160,7 +160,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 "setEscaperRuntime()" before calling %s().', __METHOD__));
+            throw new \LogicException(sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
         }
 
         $this->escaper->setSafeClasses($safeClasses);
@@ -174,7 +174,7 @@ 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 setEscaperRuntime() before calling %s().', __METHOD__));
+            throw new \LogicException(sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
         }
 
         $this->escaper->addSafeClass($class, $strategies);


Test patch:
diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php
index b23930b5..950bd1b4 100644
--- a/tests/Extension/EscaperTest.php
+++ b/tests/Extension/EscaperTest.php
@@ -28,8 +28,6 @@ class EscaperTest extends TestCase
     {
         $twig = new Environment($this->createMock(LoaderInterface::class));
         $escaperExt = $twig->getExtension(EscaperExtension::class);
-        $escaperExt->setEnvironment($twig);
-        $escaperExt->setEscaperRuntime($twig->getRuntime(EscaperRuntime::class));
         $escaperExt->setEscaper('foo', 'Twig\Tests\legacy_escaper');
         $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy));
     }
@@ -43,6 +41,19 @@ class EscaperTest extends TestCase
         ];
     }
 
+    /**
+     * @dataProvider provideCustomEscaperCases
+     *
+     * @group legacy
+     */
+    public function testCustomEscaperWithoutCallingSetEscaperRuntime($expected, $string, $strategy)
+    {
+        $twig = new Environment($this->createMock(LoaderInterface::class));
+        $escaperExt = $twig->getExtension(EscaperExtension::class);
+        $escaperExt->setEscaper('foo', 'Twig\Tests\legacy_escaper');
+        $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy));
+    }
+
     /**
      * @group legacy
      */
@@ -50,14 +61,10 @@ class EscaperTest extends TestCase
     {
         $env1 = new Environment($this->createMock(LoaderInterface::class));
         $escaperExt1 = $env1->getExtension(EscaperExtension::class);
-        $escaperExt1->setEnvironment($env1);
-        $escaperExt1->setEscaperRuntime($env1->getRuntime(EscaperRuntime::class));
         $escaperExt1->setEscaper('foo', 'Twig\Tests\legacy_escaper');
 
         $env2 = new Environment($this->createMock(LoaderInterface::class));
         $escaperExt2 = $env2->getExtension(EscaperExtension::class);
-        $escaperExt2->setEscaperRuntime($env2->getRuntime(EscaperRuntime::class));
-        $escaperExt2->setEnvironment($env2);
         $escaperExt2->setEscaper('foo', 'Twig\Tests\legacy_escaper_again');
 
         $this->assertSame('fooUTF-8', $env1->getRuntime(EscaperRuntime::class)->escape('foo', 'foo'));