From ca19ba467e82baae32ab9aa516b44b28435c0208 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 6 Apr 2016 06:22:26 -0700 Subject: [PATCH 1/3] Initial production FilesystemSource implementation See #290 --- src/Mustache/Engine.php | 22 +++-- src/Mustache/Loader.php | 2 +- .../Loader/ProductionFilesystemLoader.php | 40 +++++++++ src/Mustache/Source.php | 32 ++++++++ src/Mustache/Source/FilesystemSource.php | 64 +++++++++++++++ test/Mustache/Test/EngineTest.php | 9 ++ .../Loader/ProductionFilesystemLoaderTest.php | 82 +++++++++++++++++++ 7 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 src/Mustache/Loader/ProductionFilesystemLoader.php create mode 100644 src/Mustache/Source.php create mode 100644 src/Mustache/Source/FilesystemSource.php create mode 100644 test/Mustache/Test/Loader/ProductionFilesystemLoaderTest.php diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index e843d1e..38c05c9 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -605,12 +605,18 @@ 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 */ public function getTemplateClassName($source) { + if ($source instanceof Mustache_Source) { + $key = $source->getKey(); + } else { + $key = sprintf('source:%s', $source); + } + // For the most part, adding a new option here should do the trick. // // Pick a value here which is unique for each possible way the template @@ -629,7 +635,7 @@ class Mustache_Engine 'version' => self::VERSION, ); - return $this->templateClassPrefix . md5(json_encode($options) . "\n" . $source); + return $this->templateClassPrefix . md5(json_encode($options) . "\n" . $key); } /** @@ -706,8 +712,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 +781,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 +795,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..a79b6f4 --- /dev/null +++ b/src/Mustache/Loader/ProductionFilesystemLoader.php @@ -0,0 +1,40 @@ +getFileName($name); + + if (!file_exists($fileName)) { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return new Mustache_Source_FilesystemSource($fileName); + } +} diff --git a/src/Mustache/Source.php b/src/Mustache/Source.php new file mode 100644 index 0000000..3eded91 --- /dev/null +++ b/src/Mustache/Source.php @@ -0,0 +1,32 @@ +filename = $filename; + } + + /** + * 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() + { + 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)); + } + + return sprintf('filename:%s,size:%s,mtime:%s', $this->filename, $this->stat['size'], $this->stat['mtime']); + } + + /** + * 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'); + } +} From 1514e0c2cd538b5264ce4bdc3469f7cd3a2eab64 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 8 Apr 2016 06:56:52 -0700 Subject: [PATCH 2/3] Add stat() options to ProductionFilesystemLoader * By default, continue to use `size` and `mtime` to invalidate template cache * Allow overriding to use any properties of `stat()` * Allow disabling stat entirely * Add a Big Kid Pants disclaimer if you choose to go down this path --- .../Loader/ProductionFilesystemLoader.php | 48 ++++++++++++++++++- src/Mustache/Source/FilesystemSource.php | 31 ++++++++---- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/Mustache/Loader/ProductionFilesystemLoader.php b/src/Mustache/Loader/ProductionFilesystemLoader.php index a79b6f4..85428f0 100644 --- a/src/Mustache/Loader/ProductionFilesystemLoader.php +++ b/src/Mustache/Loader/ProductionFilesystemLoader.php @@ -18,6 +18,52 @@ */ class Mustache_Loader_ProductionFilesystemLoader extends Mustache_Loader_FilesystemLoader { + private $statProps; + + /** + * Mustache production filesystem Loader constructor. + * + * Passing an $options array allows overriding certain Loader options during instantiation: + * + * $options = array( + * // The filename extension used for Mustache templates. Defaults to '.mustache' + * 'extension' => '.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. * @@ -35,6 +81,6 @@ class Mustache_Loader_ProductionFilesystemLoader extends Mustache_Loader_Filesys throw new Mustache_Exception_UnknownTemplateException($name); } - return new Mustache_Source_FilesystemSource($fileName); + return new Mustache_Source_FilesystemSource($fileName, $this->statProps); } } diff --git a/src/Mustache/Source/FilesystemSource.php b/src/Mustache/Source/FilesystemSource.php index c54a406..7928ab2 100644 --- a/src/Mustache/Source/FilesystemSource.php +++ b/src/Mustache/Source/FilesystemSource.php @@ -19,17 +19,20 @@ */ class Mustache_Source_FilesystemSource implements Mustache_Source { - private $stat; private $filename; + private $statProps; + private $stat; /** * Filesystem Source constructor. * * @param string $filename + * @param array $statProps */ - public function __construct($filename) + public function __construct($filename, array $statProps) { $this->filename = $filename; + $this->statProps = $statProps; } /** @@ -41,15 +44,25 @@ class Mustache_Source_FilesystemSource implements Mustache_Source */ public function getKey() { - if (!isset($this->stat)) { - $this->stat = stat($this->filename); + $chunks = array( + sprintf('filename:%s', $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[] = sprintf('%s:%s', $prop, $this->stat[$prop]); + } } - if ($this->stat === false) { - throw new RuntimeException(sprintf('Failed to read source file "%s".', $this->filename)); - } - - return sprintf('filename:%s,size:%s,mtime:%s', $this->filename, $this->stat['size'], $this->stat['mtime']); + return implode(',', $chunks); } /** From 556b8e67b857afcec84916f6231862213e7f82ec Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 2 Jul 2017 21:50:12 -0700 Subject: [PATCH 3/3] Clean up Source implementation a bit. Make it work with new template class name logic. --- src/Mustache/Engine.php | 19 +++++++++++-------- src/Mustache/Source.php | 8 ++++++++ src/Mustache/Source/FilesystemSource.php | 20 ++++++++++---------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 38c05c9..41cb920 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -611,12 +611,6 @@ class Mustache_Engine */ public function getTemplateClassName($source) { - if ($source instanceof Mustache_Source) { - $key = $source->getKey(); - } else { - $key = sprintf('source:%s', $source); - } - // For the most part, adding a new option here should do the trick. // // Pick a value here which is unique for each possible way the template @@ -625,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" . $key); + $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); } /** diff --git a/src/Mustache/Source.php b/src/Mustache/Source.php index 3eded91..e6dfdf4 100644 --- a/src/Mustache/Source.php +++ b/src/Mustache/Source.php @@ -17,6 +17,12 @@ interface Mustache_Source /** * Get the Source key (used to generate the compiled class name). * + * This must return a distinct key for each template source. For example, an + * MD5 hash of the template contents would probably do the trick. The + * ProductionFilesystemLoader uses mtime and file path. If your production + * source directory is under version control, you could use the current Git + * rev and the file path... + * * @throws RuntimeException when a source file cannot be read * * @return string @@ -26,6 +32,8 @@ interface Mustache_Source /** * Get the template Source. * + * @throws RuntimeException when a source file cannot be read + * * @return string */ public function getSource(); diff --git a/src/Mustache/Source/FilesystemSource.php b/src/Mustache/Source/FilesystemSource.php index 7928ab2..1201af8 100644 --- a/src/Mustache/Source/FilesystemSource.php +++ b/src/Mustache/Source/FilesystemSource.php @@ -19,19 +19,19 @@ */ class Mustache_Source_FilesystemSource implements Mustache_Source { - private $filename; + private $fileName; private $statProps; private $stat; /** * Filesystem Source constructor. * - * @param string $filename + * @param string $fileName * @param array $statProps */ - public function __construct($filename, array $statProps) + public function __construct($fileName, array $statProps) { - $this->filename = $filename; + $this->fileName = $fileName; $this->statProps = $statProps; } @@ -45,24 +45,24 @@ class Mustache_Source_FilesystemSource implements Mustache_Source public function getKey() { $chunks = array( - sprintf('filename:%s', $this->filename), + 'fileName' => $this->fileName, ); if (!empty($this->statProps)) { if (!isset($this->stat)) { - $this->stat = stat($this->filename); + $this->stat = stat($this->fileName); } if ($this->stat === false) { - throw new RuntimeException(sprintf('Failed to read source file "%s".', $this->filename)); + throw new RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName)); } foreach ($this->statProps as $prop) { - $chunks[] = sprintf('%s:%s', $prop, $this->stat[$prop]); + $chunks[$prop] = $this->stat[$prop]; } } - return implode(',', $chunks); + return json_encode($chunks); } /** @@ -72,6 +72,6 @@ class Mustache_Source_FilesystemSource implements Mustache_Source */ public function getSource() { - return file_get_contents($this->filename); + return file_get_contents($this->fileName); } }