diff --git a/CHANGELOG b/CHANGELOG index 692295745..de9dd304e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ # 3.30.0 (2026-XX-XX) * Fix the escaping safe analysis retaining every compiled template node for the lifetime of the environment + * Speed up loading a template that the environment has already loaded * Fix `IntlExtension` letting the pattern derived from a date formatter prototype override an explicit locale * Fix `IntlExtension` not honoring the locale of a date formatter prototype configured with no date and time styles * Speed up macro calls diff --git a/src/Environment.php b/src/Environment.php index 260458c71..105689902 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -72,6 +72,10 @@ class Environment private $useYield; private $defaultRuntimeLoader; private array $hotCache = []; + /** + * @var array + */ + private array $loadedWrappers = []; /** * Constructor. @@ -255,6 +259,7 @@ class Environment { $cls = $this->getTemplateClass($name); $this->hotCache[$name] = $cls.'_'.bin2hex(random_bytes(16)); + unset($this->loadedWrappers[$cls]); if ($this->cache instanceof RemovableCacheInterface) { $this->cache->remove($name, $cls); @@ -374,7 +379,9 @@ class Environment return $name; } - return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name)); + $cls = $this->getTemplateClass($name); + + return $this->loadedWrappers[$cls] ??= new TemplateWrapper($this, $this->loadTemplate($cls, $name)); } /** diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index cc5d3f296..9c9818fbf 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -598,6 +598,63 @@ EOF FilesystemHelper::removeDir($dir); } } + + public function testLoadReusesTheWrapperOfAnAlreadyLoadedTemplate(): void + { + $twig = new Environment(new ArrayLoader(['index.twig' => 'x'])); + + $this->assertSame($twig->load('index.twig'), $twig->load('index.twig')); + } + + public function testRepeatedlyRemovingACacheDoesNotAccumulateWrappers(): void + { + $dir = sys_get_temp_dir().'/twig-wrapper-growth-test'; + if (is_dir($dir)) { + FilesystemHelper::removeDir($dir); + } + mkdir($dir.'/cache', 0777, true); + file_put_contents($dir.'/index.twig', 'x'); + + try { + $twig = new Environment(new FilesystemLoader($dir), ['cache' => $dir.'/cache', 'auto_reload' => false]); + for ($i = 0; $i < 10; ++$i) { + $twig->load('index.twig'); + $twig->removeCache('index.twig'); + } + $twig->load('index.twig'); + + $wrappers = (new \ReflectionObject($twig))->getProperty('loadedWrappers')->getValue($twig); + $this->assertCount(1, $wrappers); + } finally { + FilesystemHelper::removeDir($dir); + } + } + + public function testLoadFollowsALoaderThatResolvesANameDifferently(): void + { + $dir = sys_get_temp_dir().'/twig-loader-change-test'; + if (is_dir($dir)) { + FilesystemHelper::removeDir($dir); + } + mkdir($dir.'/a', 0777, true); + mkdir($dir.'/b', 0777, true); + file_put_contents($dir.'/a/index.twig', 'a'); + file_put_contents($dir.'/b/index.twig', 'b'); + + try { + $loader = new FilesystemLoader([$dir.'/a']); + $twig = new Environment($loader, ['cache' => false]); + + $this->assertSame('a', $twig->load('index.twig')->render([])); + + // the loader now resolves the same name to another file + $loader->prependPath($dir.'/b'); + + $this->assertSame('b', $twig->load('index.twig')->render([])); + } finally { + FilesystemHelper::removeDir($dir); + } + } } class EnvironmentTest_Extension_WithGlobals extends AbstractExtension