Repository: twigphp/Twig (php)

Issue:
Twig doesn't support alternative PHP servers like RoadRunner
Alternative PHP servers like RoadRunner don't unload PHP processes and process multiple requests per process allowing to get a significant performance increase. Unfortunately, Twig is not fully compatible with such servers because it relies on stateful classes that store some data. The intention is good - to cache data during the request. However, if the PHP process is not unloaded between HTTP requests, the internal state becomes an issue. On the second and further requests, Twig gets data from the first one which easily can be broken and make the request fail.

From my investigation, I understood that the issue is mostly in the global variables. They're cached in the `\Twig\Environment::$resolvedGlobals` property. Some extensions like EasyAdmin may add global variables conditionally. For example, if the first request doesn't meet EasyAdmin's criteria, the `ea` global variable is not loaded and not added to `$resolvedGlobals`. On the next request, Twig sees the non-null `$resolvedGlobals` and doesn't try to reload global variables. So any subsequent request that tries to render EasyAdmin's stuff will fail because the `ea` variable is not loaded.

Currently, to fix this issue, I'm using the following workaround in my project:

Define a custom Twig class:
```php
class Twig extends Environment implements EventSubscriberInterface
{
    public function onKernelTerminate(TerminateEvent $event): void
    {
        // Clear the resolvedGlobals cache to reinitialize global variables on the next RR request
        $this->resolvedGlobals = null;
    }

    #[\Override]
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::TERMINATE => 'onKernelTerminate',
        ];
    }
}
```

Add a compiler pass to replace the Twig object with the custom one:

```php
class ReplaceTwigCompilerPass implements CompilerPassInterface
{

    #[\Override]
    public function process(ContainerBuilder $container): void
    {
        $twigDefinition = $container->getDefinition('twig');
        $twigDefinition->setClass(Twig::class);
        $twigDefinition->addTag('kernel.event_subscriber');
    }
}
```

The custom class subscribes to Symfony's `kernel.terminate` event and cleans up globals cache. So on the next request, Twig properly reinitializes it with variables for the current request.

And the last point - to allow the Environment's `$resolvedGlobals` field modification, I apply the following patch with `cweagans/composer-patches`:
```diff
--- a/src/Environment.php
+++ b/src/Environment.php
@@ -57,7 +57,7 @@
     private $compiler;
     /** @var array<string, mixed> */
     private $globals = [];
-    private $resolvedGlobals;
+    protected $resolvedGlobals;
     private $loadedTemplates;
     private $strictVariables;
     private $templateClassPrefix = '__TwigTemplate_';
```

For my project, this works well. But I would be more than happy to see native support to not apply custom workarounds. The fix doesn't look complex so I believe it can be implemented quickly with no breaking changes.

Pull request:
Add the possibility to reset globals
Closes #4007


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index c0f26c20..3bc9a51d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,6 @@
 # 3.13.1 (2024-XX-XX)
 
+ * Add the possibility to reset globals via `Environment::resetGlobals()`
  * Deprecate `Environment::mergeGlobals()`
 
 # 3.13.0 (2024-09-07)
diff --git a/doc/advanced.rst b/doc/advanced.rst
index b3eaa1a2..6b3f1118 100644
--- a/doc/advanced.rst
+++ b/doc/advanced.rst
@@ -104,8 +104,8 @@ What?      Implementation difficulty? How often? When?
 Globals
 -------
 
-A global variable is like any other template variable, except that it's
-available in all templates and macros::
+Global variables are available in all templates and macros. Use ``addGlobal()``
+to add a global variable to a Twig environment::
 
     $twig = new \Twig\Environment($loader);
     $twig->addGlobal('text', new Text());
@@ -680,6 +680,16 @@ method::
         // ...
     }
 
+.. caution::
+
+    Globals are fetched once from extensions and then cached for the lifetime
+    of the Twig environment. It means that globals should not be used to store
+    values that can change during the lifetime of the Twig environment. For
+    instance, if you're using an application server like RoadRunner or
+    FrakenPHP, you should not store values related to the current context (like
+    the HTTP request). If you do so, don't forget to reset the cache between
+    requests by calling ``Environment::resetGlobals()``.
+
 Functions
 ~~~~~~~~~
 
diff --git a/src/Environment.php b/src/Environment.php
index 297efa9c..41bb518e 100644
--- a/src/Environment.php
+++ b/src/Environment.php
@@ -830,6 +830,12 @@ class Environment
         return array_merge($this->extensionSet->getGlobals(), $this->globals);
     }
 
+    public function resetGlobals(): void
+    {
+        $this->resolvedGlobals = null;
+        $this->extensionSet->resetGlobals();
+    }
+
     /**
      * @deprecated since Twig 3.13
      */
diff --git a/src/Extension/GlobalsInterface.php b/src/Extension/GlobalsInterface.php
index 6f1dfe8a..d52cd107 100644
--- a/src/Extension/GlobalsInterface.php
+++ b/src/Extension/GlobalsInterface.php
@@ -12,10 +12,7 @@
 namespace Twig\Extension;
 
 /**
- * Enables usage of the deprecated Twig\Extension\AbstractExtension::getGlobals() method.
- *
- * Explicitly implement this interface if you really need to implement the
- * deprecated getGlobals() method in your extensions.
+ * Allows Twig extensions to add globals to the context.
  *
  * @author Fabien Potencier <fabien@symfony.com>
  */
diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php
index d2848b59..28d57a41 100644
--- a/src/ExtensionSet.php
+++ b/src/ExtensionSet.php
@@ -339,6 +339,11 @@ final class ExtensionSet
         return $globals;
     }
 
+    public function resetGlobals(): void
+    {
+        $this->globals = null;
+    }
+
     public function addTest(TwigTest $test): void
     {
         if ($this->initialized) {


Test patch:
diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php
index 192b8474..b5100a0a 100644
--- a/tests/EnvironmentTest.php
+++ b/tests/EnvironmentTest.php
@@ -470,6 +470,33 @@ EOF
 
         return $loader;
     }
+
+    public function testResettingGlobals()
+    {
+        $twig = new Environment(new ArrayLoader(['index' => '']));
+        $twig->addExtension(new class() extends AbstractExtension implements GlobalsInterface {
+            public function getGlobals(): array
+            {
+                return [
+                    'global_ext' => bin2hex(random_bytes(16)),
+                ];
+            }
+        });
+
+        // Force extensions initialization
+        $twig->load('index');
+
+        // Simulate request
+        $g1 = $twig->getGlobals();
+        // Simulate another call from request 1 (the globals are cached)
+        $g2 = $twig->getGlobals();
+        $this->assertSame($g1['global_ext'], $g2['global_ext']);
+
+        // Simulate request 2
+        $twig->resetGlobals();
+        $g3 = $twig->getGlobals();
+        $this->assertNotSame($g3['global_ext'], $g2['global_ext']);
+    }
 }
 
 class EnvironmentTest_Extension_WithGlobals extends AbstractExtension