diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index e843d1e..41cb920 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -605,7 +605,7 @@ class Mustache_Engine * This method must be updated any time options are added which make it so * the same template could be parsed and compiled multiple different ways. * - * @param string $source + * @param string|Mustache_Source $source * * @return string Mustache Template class name */ @@ -619,17 +619,26 @@ class Mustache_Engine // 'default' escapes. // // Keep this list in alphabetical order :) - $options = array( + $chunks = array( 'charset' => $this->charset, 'delimiters' => $this->delimiters, 'entityFlags' => $this->entityFlags, 'escape' => isset($this->escape) ? 'custom' : 'default', + 'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source', 'pragmas' => $this->getPragmas(), 'strictCallables' => $this->strictCallables, 'version' => self::VERSION, ); - return $this->templateClassPrefix . md5(json_encode($options) . "\n" . $source); + $key = json_encode($chunks); + + // Template Source instances have already provided their own source key. For strings, just include the whole + // source string in the md5 hash. + if (!$source instanceof Mustache_Source) { + $key .= "\n" . $source; + } + + return $this->templateClassPrefix . md5($key); } /** @@ -706,8 +715,8 @@ class Mustache_Engine * @see Mustache_Engine::loadPartial * @see Mustache_Engine::loadLambda * - * @param string $source - * @param Mustache_Cache $cache (default: null) + * @param string|Mustache_Source $source + * @param Mustache_Cache $cache (default: null) * * @return Mustache_Template */ @@ -775,13 +784,12 @@ class Mustache_Engine * * @see Mustache_Compiler::compile * - * @param string $source + * @param string|Mustache_Source $source * * @return string generated Mustache template class code */ private function compile($source) { - $tree = $this->parse($source); $name = $this->getTemplateClassName($source); $this->log( @@ -790,6 +798,11 @@ class Mustache_Engine array('className' => $name) ); + if ($source instanceof Mustache_Source) { + $source = $source->getSource(); + } + $tree = $this->parse($source); + $compiler = $this->getCompiler(); $compiler->setPragmas($this->getPragmas()); diff --git a/src/Mustache/Loader.php b/src/Mustache/Loader.php index c5e1a19..b8be775 100644 --- a/src/Mustache/Loader.php +++ b/src/Mustache/Loader.php @@ -21,7 +21,7 @@ interface Mustache_Loader * * @param string $name * - * @return string Mustache Template source + * @return string|Mustache_Source Mustache Template source */ public function load($name); } diff --git a/src/Mustache/Loader/ProductionFilesystemLoader.php b/src/Mustache/Loader/ProductionFilesystemLoader.php new file mode 100644 index 0000000..85428f0 --- /dev/null +++ b/src/Mustache/Loader/ProductionFilesystemLoader.php @@ -0,0 +1,86 @@ + '.ms', + * 'stat_props' => array('size', 'mtime'), + * ); + * + * Specifying 'stat_props' overrides the stat properties used to invalidate the template cache. By default, this + * uses 'mtime' and 'size', but this can be set to any of the properties supported by stat(): + * + * http://php.net/manual/en/function.stat.php + * + * You can also disable filesystem stat entirely: + * + * $options = array('stat_props' => null); + * + * But with great power comes great responsibility. Namely, if you disable stat-based cache invalidation, + * YOU MUST CLEAR THE TEMPLATE CACHE YOURSELF when your templates change. Make it part of your build or deploy + * process so you don't forget! + * + * @throws Mustache_Exception_RuntimeException if $baseDir does not exist. + * + * @param string $baseDir Base directory containing Mustache template files. + * @param array $options Array of Loader options (default: array()) + */ + public function __construct($baseDir, array $options = array()) + { + parent::__construct($baseDir, $options); + + if (array_key_exists('stat_props', $options)) { + if (empty($options['stat_props'])) { + $this->statProps = array(); + } else { + $this->statProps = $options['stat_props']; + } + } else { + $this->statProps = array('size', 'mtime'); + } + } + + /** + * Helper function for loading a Mustache file by name. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found. + * + * @param string $name + * + * @return Mustache_Source Mustache Template source + */ + protected function loadFile($name) + { + $fileName = $this->getFileName($name); + + if (!file_exists($fileName)) { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return new Mustache_Source_FilesystemSource($fileName, $this->statProps); + } +} diff --git a/src/Mustache/Source.php b/src/Mustache/Source.php new file mode 100644 index 0000000..e6dfdf4 --- /dev/null +++ b/src/Mustache/Source.php @@ -0,0 +1,40 @@ +fileName = $fileName; + $this->statProps = $statProps; + } + + /** + * Get the Source key (used to generate the compiled class name). + * + * @throws RuntimeException when a source file cannot be read + * + * @return string + */ + public function getKey() + { + $chunks = array( + 'fileName' => $this->fileName, + ); + + if (!empty($this->statProps)) { + if (!isset($this->stat)) { + $this->stat = stat($this->fileName); + } + + if ($this->stat === false) { + throw new RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName)); + } + + foreach ($this->statProps as $prop) { + $chunks[$prop] = $this->stat[$prop]; + } + } + + return json_encode($chunks); + } + + /** + * Get the template Source. + * + * @return string + */ + public function getSource() + { + return file_get_contents($this->fileName); + } +} diff --git a/test/Mustache/Test/EngineTest.php b/test/Mustache/Test/EngineTest.php index e8743d7..eb5a27a 100644 --- a/test/Mustache/Test/EngineTest.php +++ b/test/Mustache/Test/EngineTest.php @@ -331,6 +331,15 @@ class Mustache_Test_EngineTest extends Mustache_Test_FunctionalTestCase )); } + public function testCompileFromMustacheSourceInstance() + { + $baseDir = realpath(dirname(__FILE__) . '/../../fixtures/templates'); + $mustache = new Mustache_Engine(array( + 'loader' => new Mustache_Loader_ProductionFilesystemLoader($baseDir), + )); + $this->assertEquals('one contents', $mustache->render('one')); + } + private function getLoggedMustache($level = Mustache_Logger::ERROR) { $name = tempnam(sys_get_temp_dir(), 'mustache-test'); diff --git a/test/Mustache/Test/Loader/ProductionFilesystemLoaderTest.php b/test/Mustache/Test/Loader/ProductionFilesystemLoaderTest.php new file mode 100644 index 0000000..300b6e8 --- /dev/null +++ b/test/Mustache/Test/Loader/ProductionFilesystemLoaderTest.php @@ -0,0 +1,82 @@ + '.ms')); + $this->assertInstanceOf('Mustache_Source', $loader->load('alpha')); + $this->assertEquals('alpha contents', $loader->load('alpha')->getSource()); + $this->assertInstanceOf('Mustache_Source', $loader->load('beta.ms')); + $this->assertEquals('beta contents', $loader->load('beta.ms')->getSource()); + } + + public function testTrailingSlashes() + { + $baseDir = dirname(__FILE__) . '/../../../fixtures/templates/'; + $loader = new Mustache_Loader_ProductionFilesystemLoader($baseDir); + $this->assertEquals('one contents', $loader->load('one')->getSource()); + } + + public function testConstructorWithProtocol() + { + $baseDir = realpath(dirname(__FILE__) . '/../../../fixtures/templates'); + + $loader = new Mustache_Loader_ProductionFilesystemLoader('file://' . $baseDir, array('extension' => '.ms')); + $this->assertEquals('alpha contents', $loader->load('alpha')->getSource()); + $this->assertEquals('beta contents', $loader->load('beta.ms')->getSource()); + } + + public function testLoadTemplates() + { + $baseDir = realpath(dirname(__FILE__) . '/../../../fixtures/templates'); + $loader = new Mustache_Loader_ProductionFilesystemLoader($baseDir); + $this->assertEquals('one contents', $loader->load('one')->getSource()); + $this->assertEquals('two contents', $loader->load('two.mustache')->getSource()); + } + + public function testEmptyExtensionString() + { + $baseDir = realpath(dirname(__FILE__) . '/../../../fixtures/templates'); + + $loader = new Mustache_Loader_ProductionFilesystemLoader($baseDir, array('extension' => '')); + $this->assertEquals('one contents', $loader->load('one.mustache')->getSource()); + $this->assertEquals('alpha contents', $loader->load('alpha.ms')->getSource()); + + $loader = new Mustache_Loader_ProductionFilesystemLoader($baseDir, array('extension' => null)); + $this->assertEquals('two contents', $loader->load('two.mustache')->getSource()); + $this->assertEquals('beta contents', $loader->load('beta.ms')->getSource()); + } + + /** + * @expectedException Mustache_Exception_RuntimeException + */ + public function testMissingBaseDirThrowsException() + { + new Mustache_Loader_ProductionFilesystemLoader(dirname(__FILE__) . '/not_a_directory'); + } + + /** + * @expectedException Mustache_Exception_UnknownTemplateException + */ + public function testMissingTemplateThrowsException() + { + $baseDir = realpath(dirname(__FILE__) . '/../../../fixtures/templates'); + $loader = new Mustache_Loader_ProductionFilesystemLoader($baseDir); + + $loader->load('fake'); + } +}