Skip to content

Commit

Permalink
feature #4171 Introduce ChainCache and ReadOnlyFilesystemCache (Okhoshi)
Browse files Browse the repository at this point in the history
This PR was merged into the 3.x branch.

Discussion
----------

Introduce ChainCache and ReadOnlyFilesystemCache

Following the feedback in symfony/symfony#54384, I’m proposing the two new Cache implementations in Twig directly.

Commits
-------

0dbe1d9 Introduce ChainCache and ReadOnlyFilesystemCache
  • Loading branch information
fabpot committed Aug 7, 2024
2 parents 6566066 + 0dbe1d9 commit ea4d706
Show file tree
Hide file tree
Showing 4 changed files with 469 additions and 0 deletions.
81 changes: 81 additions & 0 deletions src/Cache/ChainCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Twig\Cache;

/**
* Chains several caches together.
*
* Cached items are fetched from the first cache having them in its data store.
* They are saved and deleted in all adapters at once.
*
* @author Quentin Devos <[email protected]>
*/
final class ChainCache implements CacheInterface
{
private $caches;

/**
* @param iterable<CacheInterface> $caches The ordered list of caches used to store and fetch cached items
*/
public function __construct(iterable $caches)
{
$this->caches = $caches;
}

public function generateKey(string $name, string $className): string
{
return $className.'#'.$name;
}

public function write(string $key, string $content): void
{
$splitKey = $this->splitKey($key);

foreach ($this->caches as $cache) {
$cache->write($cache->generateKey(...$splitKey), $content);
}
}

public function load(string $key): void
{
[$name, $className] = $this->splitKey($key);

foreach ($this->caches as $cache) {
$cache->load($cache->generateKey($name, $className));

if (class_exists($className, false)) {
break;
}
}
}

public function getTimestamp(string $key): int
{
$splitKey = $this->splitKey($key);

foreach ($this->caches as $cache) {
if (0 < $timestamp = $cache->getTimestamp($cache->generateKey(...$splitKey))) {
return $timestamp;
}
}

return 0;
}

/**
* @return string[]
*/
private function splitKey(string $key): array
{
return array_reverse(explode('#', $key, 2));
}
}
25 changes: 25 additions & 0 deletions src/Cache/ReadOnlyFilesystemCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Twig\Cache;

/**
* Implements a cache on the filesystem that can only be read, not written to.
*
* @author Quentin Devos <[email protected]>
*/
class ReadOnlyFilesystemCache extends FilesystemCache
{
public function write(string $key, string $content): void
{
// Do nothing with the content, it's a read-only filesystem.
}
}
231 changes: 231 additions & 0 deletions tests/Cache/ChainTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
<?php

namespace Twig\Tests\Cache;

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use PHPUnit\Framework\TestCase;
use Twig\Cache\ChainCache;
use Twig\Cache\FilesystemCache;
use Twig\Tests\FilesystemHelper;

class ChainTest extends TestCase
{
private $classname;
private $directory;
private $cache;
private $key;

protected function setUp(): void
{
$nonce = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', random_bytes(32));
$this->classname = '__Twig_Tests_Cache_ChainTest_Template_'.$nonce;
$this->directory = sys_get_temp_dir().'/twig-test';
$this->cache = new ChainCache([
new FilesystemCache($this->directory.'/A'),
new FilesystemCache($this->directory.'/B'),
]);
$this->key = $this->cache->generateKey('_test_', $this->classname);
}

protected function tearDown(): void
{
if (file_exists($this->directory)) {
FilesystemHelper::removeDir($this->directory);
}
}

public function testLoadInA()
{
$cache = new FilesystemCache($this->directory.'/A');
$key = $cache->generateKey('_test_', $this->classname);

$dir = \dirname($key);
@mkdir($dir, 0777, true);
$this->assertDirectoryExists($dir);
$this->assertFalse(class_exists($this->classname, false));

$content = $this->generateSource();
file_put_contents($key, $content);
var_dump($key);

$this->cache->load($this->key);

$this->assertTrue(class_exists($this->classname, false));
}

public function testLoadInB()
{
$cache = new FilesystemCache($this->directory.'/B');
$key = $cache->generateKey('_test_', $this->classname);

$dir = \dirname($key);
@mkdir($dir, 0777, true);
$this->assertDirectoryExists($dir);
$this->assertFalse(class_exists($this->classname, false));

$content = $this->generateSource();
file_put_contents($key, $content);
var_dump($key);

$this->cache->load($this->key);

$this->assertTrue(class_exists($this->classname, false));
}

public function testLoadInBoth()
{
$cache = new FilesystemCache($this->directory.'/A');
$key = $cache->generateKey('_test_', $this->classname);

$dir = \dirname($key);
@mkdir($dir, 0777, true);
$this->assertDirectoryExists($dir);
$this->assertFalse(class_exists($this->classname, false));

$content = $this->generateSource();
file_put_contents($key, $content);

$cache = new FilesystemCache($this->directory.'/B');
$key = $cache->generateKey('_test_', $this->classname);

$dir = \dirname($key);
@mkdir($dir, 0777, true);
$this->assertDirectoryExists($dir);
$this->assertFalse(class_exists($this->classname, false));

$content = $this->generateSource();
file_put_contents($key, $content);

$this->cache->load($this->key);

$this->assertTrue(class_exists($this->classname, false));
}

public function testLoadMissing()
{
$this->assertFalse(class_exists($this->classname, false));

$this->cache->load($this->key);

$this->assertFalse(class_exists($this->classname, false));
}

public function testWrite()
{
$content = $this->generateSource();

$cacheA = new FilesystemCache($this->directory.'/A');
$keyA = $cacheA->generateKey('_test_', $this->classname);

$this->assertFileDoesNotExist($keyA);
$this->assertFileDoesNotExist($this->directory.'/A');

$cacheB = new FilesystemCache($this->directory.'/B');
$keyB = $cacheB->generateKey('_test_', $this->classname);

$this->assertFileDoesNotExist($keyB);
$this->assertFileDoesNotExist($this->directory.'/B');

$this->cache->write($this->key, $content);

$this->assertFileExists($this->directory.'/A');
$this->assertFileExists($keyA);
$this->assertSame(file_get_contents($keyA), $content);

$this->assertFileExists($this->directory.'/B');
$this->assertFileExists($keyB);
$this->assertSame(file_get_contents($keyB), $content);
}

public function testGetTimestampInA()
{
$cache = new FilesystemCache($this->directory.'/A');
$key = $cache->generateKey('_test_', $this->classname);

$dir = \dirname($key);
@mkdir($dir, 0777, true);
$this->assertDirectoryExists($dir);

// Create the file with a specific modification time.
touch($key, 1234567890);

$this->assertSame(1234567890, $this->cache->getTimestamp($this->key));
}

public function testGetTimestampInB()
{
$cache = new FilesystemCache($this->directory.'/B');
$key = $cache->generateKey('_test_', $this->classname);

$dir = \dirname($key);
@mkdir($dir, 0777, true);
$this->assertDirectoryExists($dir);

// Create the file with a specific modification time.
touch($key, 1234567890);

$this->assertSame(1234567890, $this->cache->getTimestamp($this->key));
}

public function testGetTimestampInBoth()
{
$cacheA = new FilesystemCache($this->directory.'/A');
$keyA = $cacheA->generateKey('_test_', $this->classname);

$dir = \dirname($keyA);
@mkdir($dir, 0777, true);
$this->assertDirectoryExists($dir);

// Create the file with a specific modification time.
touch($keyA, 1234567890);

$cacheB = new FilesystemCache($this->directory.'/B');
$keyB = $cacheB->generateKey('_test_', $this->classname);

$dir = \dirname($keyB);
@mkdir($dir, 0777, true);
$this->assertDirectoryExists($dir);

// Create the file with a specific modification time.
touch($keyB, 1234567891);

$this->assertSame(1234567890, $this->cache->getTimestamp($this->key));
}

public function testGetTimestampMissingFile()
{
$this->assertSame(0, $this->cache->getTimestamp($this->key));
}

/**
* @dataProvider provideInput
*/
public function testGenerateKey($expected, $input)
{
$cache = new ChainCache([]);
$this->assertSame($expected, $cache->generateKey($input, static::class));
}

public static function provideInput()
{
return [
['Twig\Tests\Cache\ChainTest#_test_', '_test_'],
['Twig\Tests\Cache\ChainTest#_test#with#hashtag_', '_test#with#hashtag_'],
];
}

private function generateSource()
{
return strtr('<?php class {{classname}} {}', [
'{{classname}}' => $this->classname,
]);
}
}
Loading

0 comments on commit ea4d706

Please sign in to comment.