Browse Source

Merge branch 'feature/production-filesystem-loader' into dev

Justin Hileman 8 năm trước cách đây
mục cha
commit
f459a6ed3f

+ 20 - 7
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());
 

+ 1 - 1
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);
 }

+ 86 - 0
src/Mustache/Loader/ProductionFilesystemLoader.php

@@ -0,0 +1,86 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2015 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template production filesystem Loader implementation.
+ *
+ * A production-ready FilesystemLoader, which doesn't require reading a file if it already exists in the template cache.
+ *
+ * {@inheritdoc}
+ */
+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.
+     *
+     * @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);
+    }
+}

+ 40 - 0
src/Mustache/Source.php

@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2015 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache template Source interface.
+ */
+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
+     */
+    public function getKey();
+
+    /**
+     * Get the template Source.
+     *
+     * @throws RuntimeException when a source file cannot be read
+     *
+     * @return string
+     */
+    public function getSource();
+}

+ 77 - 0
src/Mustache/Source/FilesystemSource.php

@@ -0,0 +1,77 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2015 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache template Filesystem Source.
+ *
+ * This template Source uses stat() to generate the Source key, so that using
+ * pre-compiled templates doesn't require hitting the disk to read the source.
+ * It is more suitable for production use, and is used by default in the
+ * ProductionFilesystemLoader.
+ */
+class Mustache_Source_FilesystemSource implements Mustache_Source
+{
+    private $fileName;
+    private $statProps;
+    private $stat;
+
+    /**
+     * Filesystem Source constructor.
+     *
+     * @param string $fileName
+     * @param array  $statProps
+     */
+    public function __construct($fileName, array $statProps)
+    {
+        $this->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);
+    }
+}

+ 9 - 0
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');

+ 82 - 0
test/Mustache/Test/Loader/ProductionFilesystemLoaderTest.php

@@ -0,0 +1,82 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2015 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_Loader_ProductionFilesystemLoaderTest extends PHPUnit_Framework_TestCase
+{
+    public function testConstructor()
+    {
+        $baseDir = realpath(dirname(__FILE__) . '/../../../fixtures/templates');
+        $loader = new Mustache_Loader_ProductionFilesystemLoader($baseDir, array('extension' => '.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');
+    }
+}