Skip to content
7 changes: 6 additions & 1 deletion modules/cms/classes/CmsObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,12 @@ public function getTwigContent()
*/
public function getTwigCacheKey()
{
$key = $this->getFilePath();
/*
* Twig names the compiled template class after this key and skips its freshness check once that class is
* loaded, so a long-running process (such as an Octane worker) would keep rendering the old version after
* an edit. Including a hash of the content gives each version its own class.
*/
$key = $this->getFilePath() . '?' . hash('xxh128', (string) $this->getTwigContent());

if ($event = $this->fireEvent('cmsObject.getTwigCacheKey', compact('key'), true)) {
$key = $event;
Expand Down
7 changes: 6 additions & 1 deletion modules/cms/classes/ComponentPartial.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,12 @@ public function getTwigContent()
*/
public function getTwigCacheKey()
{
return $this->getFilePath();
/*
* Twig names the compiled template class after this key and skips its freshness check once that class is
* loaded, so a long-running process (such as an Octane worker) would keep rendering the old version after
* an edit. Including a hash of the content gives each version its own class.
*/
return $this->getFilePath() . '?' . hash('xxh128', (string) $this->getTwigContent());
}

/**
Expand Down
66 changes: 66 additions & 0 deletions modules/cms/tests/classes/CmsObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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, '<p>Original.</p>');

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('<p>Original.</p>', $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 = '<p>Edited.</p>';
$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('<p>Edited.</p>', $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());
}
}
Loading