feature #1812 Extracted the filesystem cache to its own class (andrewtch, fabpot)

This PR was merged into the 1.x branch.

Discussion
----------

Extracted the filesystem cache to its own class

Twig caches the compiled PHP classes on the filesystem. This is **always** the best strategy as it allows Twig to automatically benefit from PHP opcache/APC. But overriding how the classes are stored on the filesystem is difficult with the current way, so this PR proposes to extract this logic to its own class (that should allow Drupal to stop copy/pasting some Twig code and accessing private code - see #1811).

This PR also unifies the no-cache feature by extracting it to its own class as well.

BC is kept and the `false` and `$dir` caching strategies are still supported.

The `Twig_Cache_Interface` can be used to create other cache classes, but that's not documented and should only be used with extreme care (performance-wise). It means that Twig itself will **never** ship with other implementations like Memcache, Redis, whatever storage is hype nowadays. This should fix #1421 (this PR builds on top of it), #1573, #1415, #741, #728.

Commits
-------

cacfb06 added a cache interface for templates
04cc7e4 Implemented filesystem cache, one and only (see #1415)
This commit is contained in:
Fabien Potencier
2015-09-11 16:41:19 +02:00
10 changed files with 310 additions and 59 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
* 1.21.3 (2015-XX-XX)
* 1.22.0 (2015-XX-XX)
* deprecated Twig_Environment::clearCacheFiles()
* added a way to override the filesystem template cache system
* added a way to get the original template source from Twig_Template
* 1.21.2 (2015-09-09)
+1 -1
View File
@@ -40,7 +40,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.21-dev"
"dev-master": "1.22-dev"
}
}
}
+3
View File
@@ -143,6 +143,9 @@ Miscellaneous
* As of Twig 1.x, ``Twig_Environment::clearTemplateCache()`` is deprecated and
will be removed in 2.0.
* As of Twig 1.x, ``Twig_Environment::clearCacheFiles()`` is deprecated and
will be removed in 2.0.
* As of Twig 1.x, ``Twig_Template::getEnvironment()`` and
``Twig_TemplateInterface::getEnvironment()`` are deprecated and will be
removed in 2.0.
+1 -1
View File
@@ -15,7 +15,7 @@
#ifndef PHP_TWIG_H
#define PHP_TWIG_H
#define PHP_TWIG_VERSION "1.21.3-DEV"
#define PHP_TWIG_VERSION "1.22.0-DEV"
#include "php.h"
+89
View File
@@ -0,0 +1,89 @@
<?php
/*
* This file is part of Twig.
*
* (c) 2015 Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Implements a cache on the filesystem.
*
* @author Andrew Tch <andrew@noop.lv>
*/
class Twig_Cache_Filesystem implements Twig_CacheInterface
{
private $directory;
/**
* @param $directory string The root cache directory
*/
public function __construct($directory)
{
$this->directory = $directory;
}
/**
* {@inheritdoc}
*/
public function generateKey($className, $prefix)
{
$class = substr($className, strlen($prefix));
return $this->directory.'/'.$class[0].'/'.$class[1].'/'.$class.'.php';
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return is_file($key);
}
/**
* {@inheritdoc}
*/
public function load($key)
{
require_once $key;
}
/**
* {@inheritdoc}
*/
public function write($key, $content)
{
$dir = dirname($key);
if (!is_dir($dir)) {
if (false === @mkdir($dir, 0777, true)) {
clearstatcache(false, $dir);
if (!is_dir($dir)) {
throw new RuntimeException(sprintf('Unable to create the cache directory (%s).', $dir));
}
}
} elseif (!is_writable($dir)) {
throw new RuntimeException(sprintf('Unable to write in the cache directory (%s).', $dir));
}
$tmpFile = tempnam($dir, basename($key));
if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $key)) {
@chmod($key, 0666 & ~umask());
return;
}
throw new RuntimeException(sprintf('Failed to write cache file "%s".', $key));
}
/**
* {@inheritdoc}
*/
public function getTimestamp($key)
{
return filemtime($key);
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
/*
* This file is part of Twig.
*
* (c) 2015 Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Implements a no-cache strategy.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Twig_Cache_Null implements Twig_CacheInterface
{
/**
* {@inheritdoc}
*/
public function generateKey($className, $prefix)
{
return '';
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return false;
}
/**
* {@inheritdoc}
*/
public function write($key, $content)
{
eval('?>'.$content);
}
/**
* {@inheritdoc}
*/
public function load($key)
{
}
/**
* {@inheritdoc}
*/
public function getTimestamp($key)
{
// never called as has() always returns false
return 0;
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
/*
* This file is part of Twig.
*
* (c) 2015 Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Interface implemented by cache classes.
*
* It is highly recommended to always store templates on the filesystem to
* benefit from the PHP opcode cache. This interface is mostly useful if you
* need to implement a custom strategy for storing templates on the filesystem.
*
* @author Andrew Tch <andrew@noop.lv>
*/
interface Twig_CacheInterface
{
/**
* Generates a cache key for the given template class name.
*
* @param string $className The template class name
* @param string $prefix A template class prefix
*
* @return string
*/
public function generateKey($className, $prefix);
/**
* Checks if the cache key exists.
*
* @param string $key The cache key
*
* @return bool true if the cache key exists, false otherwise
*/
public function has($key);
/**
* Writes the compiled template to cache.
*
* @param string $key The cache key
* @param string $content The template representation as a PHP class
*/
public function write($key, $content);
/**
* Loads a template from the cache.
*
* @param string $key The cache key
*/
public function load($key);
/**
* Returns the modification timestamp of a key.
*
* @param string $key The cache key
*
* @return int
*/
public function getTimestamp($key);
}
+78 -49
View File
@@ -16,7 +16,7 @@
*/
class Twig_Environment
{
const VERSION = '1.21.3-DEV';
const VERSION = '1.22.0-DEV';
protected $charset;
protected $loader;
@@ -45,6 +45,10 @@ class Twig_Environment
protected $filterCallbacks = array();
protected $staging;
private $originalCache;
private $bcWriteCacheFile = false;
private $bcGetCacheFilename = false;
/**
* Constructor.
*
@@ -58,8 +62,9 @@ class Twig_Environment
* * base_template_class: The base template class to use for generated
* templates (default to Twig_Template).
*
* * cache: An absolute path where to store the compiled templates, or
* false to disable compilation cache (default).
* * cache: An absolute path where to store the compiled templates,
* a Twig_Cache_Interface implementation,
* or false to disable compilation cache (default).
*
* * auto_reload: Whether to reload the template if the original source changed.
* If you don't provide the auto_reload option, it will be
@@ -112,6 +117,23 @@ class Twig_Environment
$this->addExtension(new Twig_Extension_Escaper($options['autoescape']));
$this->addExtension(new Twig_Extension_Optimizer($options['optimizations']));
$this->staging = new Twig_Extension_Staging();
// For BC
if (is_string($this->originalCache)) {
$r = new ReflectionMethod($this, 'writeCacheFile');
if ($r->getDeclaringClass()->getName() !== __CLASS__) {
@trigger_error('The Twig_Environment::writeCacheFile method is deprecated and will be removed in Twig 2.0.', E_USER_DEPRECATED);
$this->bcWriteCacheFile = true;
}
$r = new ReflectionMethod($this, 'getCacheFilename');
if ($r->getDeclaringClass()->getName() !== __CLASS__) {
@trigger_error('The Twig_Environment::getCacheFilename method is deprecated and will be removed in Twig 2.0.', E_USER_DEPRECATED);
$this->bcGetCacheFilename = true;
}
}
}
/**
@@ -213,24 +235,39 @@ class Twig_Environment
}
/**
* Gets the cache directory or false if cache is disabled.
* Gets the current cache implementation.
*
* @return string|false
* @param bool $original Whether to return the original cache option or the real cache instance
*
* @return Twig_CacheInterface|string|false A Twig_CacheInterface implementation,
* an absolute path to the compiled templates,
* or false to disable cache
*/
public function getCache()
public function getCache($original = true)
{
return $this->cache;
return $original ? $this->originalCache : $this->cache;
}
/**
* Sets the cache directory or false if cache is disabled.
* Sets the current cache implementation.
*
* @param string|false $cache The absolute path to the compiled templates,
* or false to disable cache
* @param Twig_CacheInterface|string|false $cache A Twig_CacheInterface implementation,
* an absolute path to the compiled templates,
* or false to disable cache
*/
public function setCache($cache)
{
$this->cache = $cache ? $cache : false;
if (is_string($cache)) {
$this->originalCache = $cache;
$this->cache = new Twig_Cache_Filesystem($cache);
} elseif (false === $cache) {
$this->originalCache = $cache;
$this->cache = new Twig_Cache_Null();
} elseif ($cache instanceof Twig_CacheInterface) {
$this->originalCache = $this->cache = $cache;
} else {
throw new LogicException(sprintf('Cache can only be a string, false, or a Twig_CacheInterface implementation.'));
}
}
/**
@@ -239,16 +276,16 @@ class Twig_Environment
* @param string $name The template name
*
* @return string|false The cache file name or false when caching is disabled
*
* @deprecated since 1.22 (to be removed in 2.0)
*/
public function getCacheFilename($name)
{
if (false === $this->cache) {
return false;
}
@trigger_error(sprintf('The %s method is deprecated and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
$class = substr($this->getTemplateClass($name), strlen($this->templateClassPrefix));
$key = $this->cache->generateKey($this->getTemplateClass($name), $this->templateClassPrefix);
return $this->getCache().'/'.$class[0].'/'.$class[1].'/'.$class.'.php';
return !$key ? false : $key;
}
/**
@@ -326,15 +363,21 @@ class Twig_Environment
}
if (!class_exists($cls, false)) {
if (false === $cache = $this->getCacheFilename($name)) {
eval('?>'.$this->compileSource($this->getLoader()->getSource($name), $name));
if ($this->bcGetCacheFilename) {
$key = $this->getCacheFilename($name);
} else {
if (!is_file($cache) || ($this->isAutoReload() && !$this->isTemplateFresh($name, filemtime($cache)))) {
$this->writeCacheFile($cache, $this->compileSource($this->getLoader()->getSource($name), $name));
}
require_once $cache;
$key = $this->cache->generateKey($cls, $this->templateClassPrefix);
}
if (!$this->cache->has($key) || ($this->isAutoReload() && !$this->isTemplateFresh($name, $this->cache->getTimestamp($key)))) {
if ($this->bcWriteCacheFile) {
$this->writeCacheFile($key, $this->compileSource($this->getLoader()->getSource($name), $name));
} else {
$this->cache->write($key, $this->compileSource($this->getLoader()->getSource($name), $name));
}
}
$this->cache->load($key);
}
if (!$this->runtimeInitialized) {
@@ -453,16 +496,18 @@ class Twig_Environment
/**
* Clears the template cache files on the filesystem.
*
* @deprecated since 1.22 (to be removed in 2.0)
*/
public function clearCacheFiles()
{
if (false === $this->cache) {
return;
}
@trigger_error(sprintf('The %s method is deprecated and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->cache), RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
if ($file->isFile()) {
@unlink($file->getPathname());
if (is_string($this->originalCache)) {
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->originalCache), RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
if ($file->isFile()) {
@unlink($file->getPathname());
}
}
}
}
@@ -1278,27 +1323,11 @@ class Twig_Environment
}
}
/**
* @deprecated since 1.22 (to be removed in 2.0)
*/
protected function writeCacheFile($file, $content)
{
$dir = dirname($file);
if (!is_dir($dir)) {
if (false === @mkdir($dir, 0777, true)) {
clearstatcache(false, $dir);
if (!is_dir($dir)) {
throw new RuntimeException(sprintf('Unable to create the cache directory (%s).', $dir));
}
}
} elseif (!is_writable($dir)) {
throw new RuntimeException(sprintf('Unable to write in the cache directory (%s).', $dir));
}
$tmpFile = tempnam($dir, basename($file));
if ((false !== @file_put_contents($tmpFile, $content)) && @rename($tmpFile, $file)) {
@chmod($file, 0666 & ~umask());
return;
}
throw new RuntimeException(sprintf('Failed to write cache file "%s".', $file));
$this->cache->write($file, $content);
}
}
+6 -7
View File
@@ -150,15 +150,14 @@ class Twig_Tests_EnvironmentTest extends PHPUnit_Framework_TestCase
public function testExtensionsAreNotInitializedWhenRenderingACompiledTemplate()
{
$options = array('cache' => sys_get_temp_dir().'/twig', 'auto_reload' => false, 'debug' => false);
$cache = new Twig_Cache_Filesystem($dir = sys_get_temp_dir().'/twig');
$options = array('cache' => $cache, 'auto_reload' => false, 'debug' => false);
// force compilation
$twig = new Twig_Environment($loader = new Twig_Loader_Array(array('index' => '{{ foo }}')), $options);
$cache = $twig->getCacheFilename('index');
if (!is_dir(dirname($cache))) {
mkdir(dirname($cache), 0777, true);
}
file_put_contents($cache, $twig->compileSource('{{ foo }}', 'index'));
$key = $cache->generateKey($twig->getTemplateClass('index'), $twig->getTemplateClassPrefix());
$cache->write($key, $twig->compileSource('{{ foo }}', 'index'));
// check that extensions won't be initialized when rendering a template that is already in the cache
$twig = $this
@@ -174,7 +173,7 @@ class Twig_Tests_EnvironmentTest extends PHPUnit_Framework_TestCase
$output = $twig->render('index', array('foo' => 'bar'));
$this->assertEquals('bar', $output);
unlink($cache);
unlink($key);
}
public function testAddExtension()
+6
View File
@@ -38,6 +38,9 @@ class Twig_Tests_FileCachingTest extends PHPUnit_Framework_TestCase
$this->removeDir($this->tmpDir);
}
/**
* @group legacy
*/
public function testWritingCacheFiles()
{
$name = 'index';
@@ -48,6 +51,9 @@ class Twig_Tests_FileCachingTest extends PHPUnit_Framework_TestCase
$this->fileName = $cacheFileName;
}
/**
* @group legacy
*/
public function testClearingCacheFiles()
{
$name = 'index2';