mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-17 21:07:18 +00:00
feature #4338 Add template cache hot reload (fabpot)
This PR was squashed before being merged into the 3.x branch. Discussion ---------- Add template cache hot reload Closes #3880 This PR achieves 2 things in the minimum amount of code (in 2 commits): * It adds a new `RemovableCacheInterface` that allows cache to be removed. * It adds a memory cache to allow updating existing templates in the same process (think FrakenPHP for instance) without polluting the permanent cache. Commits -------00ba8046b0Add testdca5099aadAdd hot cache reload for templates786a67b1a9Add RemovableCacheInterface
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# 3.15.0 (2024-XX-XX)
|
||||
|
||||
* Add template cache hot reload
|
||||
* Allow Twig callable argument names to be free-form (snake-case or camelCase) independently of the PHP callable signature
|
||||
They were automatically converted to snake-cased before
|
||||
* Deprecate the `attribute` function; use the `.` notation and wrap the name with parenthesis instead
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Twig\Cache;
|
||||
*
|
||||
* @author Quentin Devos <quentin@devos.pm>
|
||||
*/
|
||||
final class ChainCache implements CacheInterface
|
||||
final class ChainCache implements CacheInterface, RemovableCacheInterface
|
||||
{
|
||||
/**
|
||||
* @param iterable<CacheInterface> $caches The ordered list of caches used to store and fetch cached items
|
||||
@@ -69,6 +69,15 @@ final class ChainCache implements CacheInterface
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function remove(string $name, string $cls): void
|
||||
{
|
||||
foreach ($this->caches as $cache) {
|
||||
if ($cache instanceof RemovableCacheInterface) {
|
||||
$cache->remove($name, $cls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Twig\Cache;
|
||||
*
|
||||
* @author Andrew Tch <andrew@noop.lv>
|
||||
*/
|
||||
class FilesystemCache implements CacheInterface
|
||||
class FilesystemCache implements CacheInterface, RemovableCacheInterface
|
||||
{
|
||||
public const FORCE_BYTECODE_INVALIDATION = 1;
|
||||
|
||||
@@ -76,6 +76,14 @@ class FilesystemCache implements CacheInterface
|
||||
throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $key));
|
||||
}
|
||||
|
||||
public function remove(string $name, string $cls): void
|
||||
{
|
||||
$key = $this->generateKey($name, $cls);
|
||||
if (!@unlink($key) && file_exists($key)) {
|
||||
throw new \RuntimeException(\sprintf('Failed to delete cache file "%s".', $key));
|
||||
}
|
||||
}
|
||||
|
||||
public function getTimestamp(string $key): int
|
||||
{
|
||||
if (!is_file($key)) {
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Twig\Cache;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class NullCache implements CacheInterface
|
||||
final class NullCache implements CacheInterface, RemovableCacheInterface
|
||||
{
|
||||
public function generateKey(string $name, string $className): string
|
||||
{
|
||||
@@ -35,4 +35,8 @@ final class NullCache implements CacheInterface
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function remove(string $name, string $cls): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface RemovableCacheInterface
|
||||
{
|
||||
public function remove(string $name, string $cls): void;
|
||||
}
|
||||
+19
-3
@@ -14,6 +14,7 @@ namespace Twig;
|
||||
use Twig\Cache\CacheInterface;
|
||||
use Twig\Cache\FilesystemCache;
|
||||
use Twig\Cache\NullCache;
|
||||
use Twig\Cache\RemovableCacheInterface;
|
||||
use Twig\Error\Error;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
@@ -71,6 +72,7 @@ class Environment
|
||||
/** @var bool */
|
||||
private $useYield;
|
||||
private $defaultRuntimeLoader;
|
||||
private array $hotCache = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -233,6 +235,18 @@ class Environment
|
||||
return $this->strictVariables;
|
||||
}
|
||||
|
||||
public function removeCache(string $name): void
|
||||
{
|
||||
$cls = $this->getTemplateClass($name);
|
||||
$this->hotCache[$name] = $cls.'_'.bin2hex(random_bytes(16));
|
||||
|
||||
if ($this->cache instanceof RemovableCacheInterface) {
|
||||
$this->cache->remove($name, $cls);
|
||||
} else {
|
||||
throw new \LogicException(\sprintf('The "%s" cache class does not support removing template cache as it does not implement the "RemovableCacheInterface" interface.', \get_class($this->cache)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current cache implementation.
|
||||
*
|
||||
@@ -287,7 +301,7 @@ class Environment
|
||||
*/
|
||||
public function getTemplateClass(string $name, ?int $index = null): string
|
||||
{
|
||||
$key = $this->getLoader()->getCacheKey($name).$this->optionsHash;
|
||||
$key = ($this->hotCache[$name] ?? $this->getLoader()->getCacheKey($name)).$this->optionsHash;
|
||||
|
||||
return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index);
|
||||
}
|
||||
@@ -379,8 +393,10 @@ class Environment
|
||||
if (!class_exists($cls, false)) {
|
||||
$source = $this->getLoader()->getSourceContext($name);
|
||||
$content = $this->compileSource($source);
|
||||
$this->cache->write($key, $content);
|
||||
$this->cache->load($key);
|
||||
if (!isset($this->hotCache[$name])) {
|
||||
$this->cache->write($key, $content);
|
||||
$this->cache->load($key);
|
||||
}
|
||||
|
||||
if (!class_exists($mainCls, false)) {
|
||||
/* Last line of defense if either $this->bcWriteCacheFile was used,
|
||||
|
||||
@@ -22,6 +22,7 @@ use Twig\Extension\AbstractExtension;
|
||||
use Twig\Extension\ExtensionInterface;
|
||||
use Twig\Extension\GlobalsInterface;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Twig\Loader\LoaderInterface;
|
||||
use Twig\Node\Node;
|
||||
use Twig\NodeVisitor\NodeVisitorInterface;
|
||||
@@ -497,6 +498,53 @@ EOF
|
||||
$g3 = $twig->getGlobals();
|
||||
$this->assertNotSame($g3['global_ext'], $g2['global_ext']);
|
||||
}
|
||||
|
||||
public function testHotCache()
|
||||
{
|
||||
$dir = sys_get_temp_dir().'/twig-hot-cache-test';
|
||||
if (is_dir($dir)) {
|
||||
FilesystemHelper::removeDir($dir);
|
||||
}
|
||||
mkdir($dir);
|
||||
file_put_contents($dir.'/index.twig', 'x');
|
||||
try {
|
||||
$twig = new Environment(new FilesystemLoader($dir), [
|
||||
'debug' => false,
|
||||
'auto_reload' => false,
|
||||
'cache' => $dir.'/cache',
|
||||
]);
|
||||
|
||||
// prime the cache
|
||||
$this->assertSame('x', $twig->load('index.twig')->render([]));
|
||||
|
||||
// update the template
|
||||
file_put_contents($dir.'/index.twig', 'y');
|
||||
|
||||
// re-render, should use the cached version
|
||||
$this->assertSame('x', $twig->load('index.twig')->render([]));
|
||||
|
||||
// clear the cache
|
||||
$twig->removeCache('index.twig');
|
||||
|
||||
// re-render, should use the updated template
|
||||
$this->assertSame('y', $twig->load('index.twig')->render([]));
|
||||
|
||||
// the new template should not be cached
|
||||
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir.'/cache', \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST);
|
||||
$count = 0;
|
||||
foreach ($iterator as $fileInfo) {
|
||||
if (!$fileInfo->isDir()) {
|
||||
++$count;
|
||||
}
|
||||
}
|
||||
$this->assertSame(0, $count);
|
||||
|
||||
// re-render, should use the updated template
|
||||
$this->assertSame('y', $twig->load('index.twig')->render([]));
|
||||
} finally {
|
||||
FilesystemHelper::removeDir($dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EnvironmentTest_Extension_WithGlobals extends AbstractExtension
|
||||
|
||||
Reference in New Issue
Block a user