diff --git a/src/Mustache/Cache.php b/src/Mustache/Cache.php new file mode 100644 index 0000000..1ca7438 --- /dev/null +++ b/src/Mustache/Cache.php @@ -0,0 +1,36 @@ +logger; + } + + public function setLogger($logger = null) + { + if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) { + throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.'); + } + + $this->logger = $logger; + } + + protected function log($level, $message, array $context = array()) + { + if (isset($this->logger)) { + $this->logger->log($level, $message, $context); + } + } +} diff --git a/src/Mustache/Cache/FilesystemCache.php b/src/Mustache/Cache/FilesystemCache.php new file mode 100644 index 0000000..ade547d --- /dev/null +++ b/src/Mustache/Cache/FilesystemCache.php @@ -0,0 +1,153 @@ +cache($className, $compiledSource); + * + * Benefits from any opcode caching that may be setup in your environment. + */ +class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache +{ + private $baseDir; + private $fileMode; + + /** + * Filesystem cache constructor. + * + * @param string $baseDir Directory for compiled templates. + * @param int $fileMode Override default permissions for cache files. Defaults to using the system-defined umask. + */ + public function __construct($baseDir, $fileMode = null) + { + $this->baseDir = $baseDir; + $this->fileMode = $fileMode; + } + + /** + * Load the class from cache using `require_once`. + * + * @param string $key + * @return boolean + */ + public function load($key) + { + $fileName = $this->getCacheFilename($key); + if (!is_file($fileName)) { + return false; + } + + require_once $fileName; + + return true; + } + + /** + * Cache and load the compiled class + * + * @param string $key + * @param string $value + * @return void + */ + public function cache($key, $value) + { + $fileName = $this->getCacheFilename($key); + + $this->log( + Mustache_Logger::DEBUG, + 'Writing to template cache: "{fileName}"', + array('fileName' => $fileName) + ); + + $this->writeFile($fileName, $value); + $this->load($key); + } + + /** + * Build the cache filename. + * Subclasses should override for custom cache directory structures. + * + * @param string $name + * @return string + */ + protected function getCacheFilename($name) + { + return sprintf('%s/%s.php', $this->baseDir, $name); + } + + /** + * Create cache directory + * + * @param string $fileName + * @return string + * + * @throws Mustache_Exception_RuntimeException If unable to create directory + */ + private function buildDirectoryForFilename($fileName) + { + $dirName = dirname($fileName); + if (!is_dir($dirName)) { + $this->log( + Mustache_Logger::INFO, + 'Creating Mustache template cache directory: "{dirName}"', + array('dirName' => $dirName) + ); + + @mkdir($dirName, 0777, true); + if (!is_dir($dirName)) { + throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName)); + } + } + return $dirName; + } + + /** + * Write cache file + * + * @param string $fileName + * @param string $value + * @return void + * + * @throws Mustache_Exception_RuntimeException If unable to write file + */ + private function writeFile($fileName, $value) + { + $dirName = $this->buildDirectoryForFilename($fileName); + + $this->log( + Mustache_Logger::DEBUG, + 'Caching compiled template to "{fileName}"', + array('fileName' => $fileName) + ); + + $tempFile = tempnam($dirName, basename($fileName)); + if (false !== @file_put_contents($tempFile, $value)) { + if (@rename($tempFile, $fileName)) { + $mode = isset($this->fileMode) ? $this->fileMode : (0666 & ~umask()); + @chmod($fileName, $mode); + + return; + } + + $this->log( + Mustache_Logger::ERROR, + 'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"', + array('tempName' => $tempFile, 'fileName' => $fileName) + ); + } + + throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName)); + } +} diff --git a/src/Mustache/Cache/NoopCache.php b/src/Mustache/Cache/NoopCache.php new file mode 100644 index 0000000..bd54362 --- /dev/null +++ b/src/Mustache/Cache/NoopCache.php @@ -0,0 +1,47 @@ +log( + Mustache_Logger::WARNING, + 'Template cache disabled, evaluating "{className}" class at runtime', + array('className' => $key) + ); + eval("?>".$compiled); + } +} diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 5bf9c4d..488dd84 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -33,8 +33,7 @@ class Mustache_Engine // Environment private $templateClassPrefix = '__Mustache_'; - private $cache = null; - private $cacheFileMode = null; + private $cache; private $loader; private $partialsLoader; private $helpers; @@ -53,7 +52,8 @@ class Mustache_Engine * // The class prefix for compiled templates. Defaults to '__Mustache_'. * 'template_class_prefix' => '__MyTemplates_', * - * // A cache directory for compiled templates. Mustache will not cache templates unless this is set + * // A Mustache cache instance or a cache directory string for compiled templates. + * // Mustache will not cache templates unless this is set * 'cache' => dirname(__FILE__).'/tmp/cache/mustache', * * // Override default permissions for cache files. Defaults to using the system-defined umask. It is @@ -112,11 +112,14 @@ class Mustache_Engine } if (isset($options['cache'])) { - $this->cache = $options['cache']; - } + $cache = $options['cache']; - if (isset($options['cache_file_mode'])) { - $this->cacheFileMode = $options['cache_file_mode']; + if (is_string($cache)) { + $mode = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null; + $cache = new Mustache_Cache_FilesystemCache($cache, $mode); + } + + $this->setCache($cache); } if (isset($options['loader'])) { @@ -388,6 +391,10 @@ class Mustache_Engine throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.'); } + if ($this->getCache()->getLogger() === null) { + $this->getCache()->setLogger($logger); + } + $this->logger = $logger; } @@ -479,6 +486,36 @@ class Mustache_Engine return $this->compiler; } + /** + * Set the Mustache Cache instance. + * + * @param Mustache_Cache $cache + */ + public function setCache(Mustache_Cache $cache) + { + if (isset($this->logger) && $cache->getLogger() === null) { + $cache->setLogger($this->getLogger()); + } + + $this->cache = $cache; + } + + /** + * Get the current Mustache Cache instance. + * + * If no Cache instance has been explicitly specified, this method will instantiate and return a new one. + * + * @return Mustache_Cache + */ + public function getCache() + { + if (!isset($this->cache)) { + $this->setCache(new Mustache_Cache_NoopCache()); + } + + return $this->cache; + } + /** * Helper method to generate a Mustache template class. * @@ -580,26 +617,9 @@ class Mustache_Engine if (!isset($this->templates[$className])) { if (!class_exists($className, false)) { - if ($fileName = $this->getCacheFilename($source)) { - if (!is_file($fileName)) { - $this->log( - Mustache_Logger::DEBUG, - 'Writing "{className}" class to template cache: "{fileName}"', - array('className' => $className, 'fileName' => $fileName) - ); - - $this->writeCacheFile($fileName, $this->compile($source)); - } - - require_once $fileName; - } else { - $this->log( - Mustache_Logger::WARNING, - 'Template cache disabled, evaluating "{className}" class at runtime', - array('className' => $className) - ); - - eval('?>'.$this->compile($source)); + if (!$this->getCache()->load($className)) { + $compiled = $this->compile($source); + $this->getCache()->cache($className, $compiled); } } @@ -666,72 +686,6 @@ class Mustache_Engine return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags); } - /** - * Helper method to generate a Mustache Template class cache filename. - * - * @param string $source - * - * @return string Mustache Template class cache filename - */ - private function getCacheFilename($source) - { - if ($this->cache) { - return sprintf('%s/%s.php', $this->cache, $this->getTemplateClassName($source)); - } - } - - /** - * Helper method to dump a generated Mustache Template subclass to the file cache. - * - * @throws Mustache_Exception_RuntimeException if unable to create the cache directory or write to $fileName. - * - * @param string $fileName - * @param string $source - * - * @codeCoverageIgnore - */ - private function writeCacheFile($fileName, $source) - { - $dirName = dirname($fileName); - if (!is_dir($dirName)) { - $this->log( - Mustache_Logger::INFO, - 'Creating Mustache template cache directory: "{dirName}"', - array('dirName' => $dirName) - ); - - @mkdir($dirName, 0777, true); - if (!is_dir($dirName)) { - throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName)); - } - - } - - $this->log( - Mustache_Logger::DEBUG, - 'Caching compiled template to "{fileName}"', - array('fileName' => $fileName) - ); - - $tempFile = tempnam($dirName, basename($fileName)); - if (false !== @file_put_contents($tempFile, $source)) { - if (@rename($tempFile, $fileName)) { - $mode = isset($this->cacheFileMode) ? $this->cacheFileMode : (0666 & ~umask()); - @chmod($fileName, $mode); - - return; - } - - $this->log( - Mustache_Logger::ERROR, - 'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"', - array('tempName' => $tempFile, 'fileName' => $fileName) - ); - } - - throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName)); - } - /** * Add a log record if logging is enabled. * diff --git a/test/Mustache/Test/Cache/FilesystemCacheTest.php b/test/Mustache/Test/Cache/FilesystemCacheTest.php new file mode 100644 index 0000000..07e4a12 --- /dev/null +++ b/test/Mustache/Test/Cache/FilesystemCacheTest.php @@ -0,0 +1,67 @@ +load($key); + + $this->assertFalse($loaded); + } + + public function testCachePut() + { + $key = 'some key'; + $value = 'cache($key, $value); + $loaded = $cache->load($key); + + $this->assertTrue($loaded); + } + + private static function rmdir($path) + { + $path = rtrim($path, '/').'/'; + $handle = opendir($path); + while (($file = readdir($handle)) !== false) { + if ($file == '.' || $file == '..') { + continue; + } + + $fullpath = $path.$file; + if (is_dir($fullpath)) { + self::rmdir($fullpath); + } else { + unlink($fullpath); + } + } + + closedir($handle); + rmdir($path); + } +} diff --git a/test/Mustache/Test/EngineTest.php b/test/Mustache/Test/EngineTest.php index 5c00821..f4d102d 100644 --- a/test/Mustache/Test/EngineTest.php +++ b/test/Mustache/Test/EngineTest.php @@ -58,6 +58,7 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase $this->assertTrue($mustache->hasHelper('foo')); $this->assertTrue($mustache->hasHelper('bar')); $this->assertFalse($mustache->hasHelper('baz')); + $this->assertInstanceOf('Mustache_Cache_FilesystemCache', $mustache->getCache()); } public static function getFoo() @@ -95,6 +96,7 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase $parser = new Mustache_Parser; $compiler = new Mustache_Compiler; $mustache = new Mustache_Engine; + $cache = new Mustache_Cache_FilesystemCache(self::$tempDir); $this->assertNotSame($logger, $mustache->getLogger()); $mustache->setLogger($logger); @@ -119,6 +121,10 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase $this->assertNotSame($compiler, $mustache->getCompiler()); $mustache->setCompiler($compiler); $this->assertSame($compiler, $mustache->getCompiler()); + + $this->assertNotSame($cache, $mustache->getCache()); + $mustache->setCache($cache); + $this->assertSame($cache, $mustache->getCache()); } /** @@ -134,10 +140,8 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase $source = '{{ foo }}'; $template = $mustache->loadTemplate($source); $className = $mustache->getTemplateClassName($source); - $fileName = self::$tempDir . '/' . $className . '.php'; + $this->assertInstanceOf($className, $template); - $this->assertFileExists($fileName); - $this->assertContains("\nclass $className extends Mustache_Template", file_get_contents($fileName)); } /**