diff --git a/modules/cms/classes/CmsObject.php b/modules/cms/classes/CmsObject.php index 4411d61d94..bee81544e8 100644 --- a/modules/cms/classes/CmsObject.php +++ b/modules/cms/classes/CmsObject.php @@ -306,7 +306,7 @@ public function getTwigContent() */ public function getTwigCacheKey() { - $key = $this->getFilePath(); + $key = $this->getFilePath() . '?' . hash('xxh128', (string) $this->getTwigContent()); if ($event = $this->fireEvent('cmsObject.getTwigCacheKey', compact('key'), true)) { $key = $event; diff --git a/modules/cms/classes/ComponentPartial.php b/modules/cms/classes/ComponentPartial.php index 3df5200011..27444ef893 100644 --- a/modules/cms/classes/ComponentPartial.php +++ b/modules/cms/classes/ComponentPartial.php @@ -197,7 +197,7 @@ public function getTwigContent() */ public function getTwigCacheKey() { - return $this->getFilePath(); + return $this->getFilePath() . '?' . hash('xxh128', (string) $this->getTwigContent()); } /** diff --git a/modules/cms/tests/classes/CmsObjectTest.php b/modules/cms/tests/classes/CmsObjectTest.php index 02b83c1170..dc570ba783 100644 --- a/modules/cms/tests/classes/CmsObjectTest.php +++ b/modules/cms/tests/classes/CmsObjectTest.php @@ -5,7 +5,11 @@ use System\Tests\Bootstrap\TestCase; use Cms\Classes\CmsObject; use Cms\Classes\Theme; +use Cms\Twig\Loader as TwigLoader; use PHPUnit\Framework\Attributes\Depends; +use Twig\Cache\FilesystemCache as TwigCacheFilesystem; +use Twig\Environment as TwigEnvironment; +use Winter\Storm\Support\Facades\File; class TestCmsObject extends CmsObject { @@ -335,4 +339,66 @@ public function testSaveNewDir() $this->assertFileExists($destFilePath); $this->assertEquals($testContents, file_get_contents($destFilePath)); } + + public function testEditedObjectRendersItsNewContentInTheSameProcess() + { + $theme = Theme::load('test'); + $filePath = $theme->getPath() . '/temporary/twig-cache-key.htm'; + $cachePath = storage_path('temp/twig-cache-key-test'); + file_put_contents($filePath, '
Original.
'); + + try { + /* + * One Twig environment for the whole test, configured the way Cms\ServiceProvider configures it. + * A long-running process such as a queue worker or an Octane worker keeps the environment, and + * every compiled template class it has loaded, between requests. + */ + $loader = new TwigLoader(); + $twig = new TwigEnvironment($loader, [ + 'auto_reload' => true, + 'cache' => new TwigCacheFilesystem($cachePath), + ]); + + /* + * First request: compile and render the original content. + */ + $obj = TestTemporaryCmsObject::load($theme, 'twig-cache-key.htm'); + $loader->setObject($obj); + $this->assertSame('Original.
', $twig->load($obj->getFilePath())->render([])); + + /* + * An editor saves new content. The save lands in the same second as the first render, so the + * file's modification time may not change. + */ + $obj->content = 'Edited.
'; + $obj->save(); + clearstatcache(); + + /* + * Next request: load the object from disk again and render it with the same environment. If the + * cache key were unchanged, Twig would reuse the class it already loaded and render the original. + */ + CmsObject::clearInternalCache(); + $obj = TestTemporaryCmsObject::load($theme, 'twig-cache-key.htm'); + $loader->setObject($obj); + $this->assertSame('Edited.
', $twig->load($obj->getFilePath())->render([])); + } finally { + @unlink($filePath); + File::deleteDirectory($cachePath); + } + } + + public function testTwigCacheKeyCanBeOverriddenByEvent() + { + $theme = Theme::load('test'); + $obj = TestCmsObject::load($theme, 'plain.html'); + + $this->assertStringStartsWith($obj->getFilePath() . '?', $obj->getTwigCacheKey()); + + $obj->bindEvent('cmsObject.getTwigCacheKey', function ($key) { + return 'custom-key'; + }); + + $this->assertSame('custom-key', $obj->getTwigCacheKey()); + } }