Split TemplateLoader's cache business into another interface and class.
authorRobert Sesek <rsesek@bluestatic.org>
Wed, 29 May 2013 02:45:22 +0000 (22:45 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Wed, 29 May 2013 02:49:03 +0000 (22:49 -0400)
This creates the CacheBackend interface, which will allow for a database cache,
and one concrete implementation FileCacheBackend. This implementation implements
the existing TemplateLoader cache behavior.

CHANGES
testing/tests/views/file_cache_backend_test.php [new file with mode: 0644]
testing/tests/views/template_loader_test.php
views/cache_backend.php [new file with mode: 0644]
views/file_cache_backend.php [new file with mode: 0644]
views/template_loader.php

diff --git a/CHANGES b/CHANGES
index c88a8044cdd52611cf2dc991207e28549055c32a..1322407202beaf97663e0ca3e2cffd6b5ee38f81 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,8 +1,11 @@
-Version 1.1
+Version 2.0
 ================================================================================
-- Add hoplite\data\ProfilingPDO to add profiling to database access.
-- Add hoplite\base\Profiling static utility class.
-- Add hoplite\http\Input for request data sanitization.
+- Split caching logic out of views\TemplateLoader into an interface
+  views\CacheBackend and views\FileCacheBackend.
+- Add template usage profiling to views\.
+- Add data\ProfilingPDO to add profiling to database access.
+- Add base\Profiling static utility class.
+- Add http\Input for request data sanitization.
 
 Version 1.0
 ================================================================================
diff --git a/testing/tests/views/file_cache_backend_test.php b/testing/tests/views/file_cache_backend_test.php
new file mode 100644 (file)
index 0000000..bb01f06
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+// Hoplite
+// Copyright (c) 2013 Blue Static
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace hoplite\test;
+use hoplite\views as views;
+
+require_once HOPLITE_ROOT . '/views/file_cache_backend.php';
+
+class TestFileCacheBackend extends views\FileCacheBackend
+{
+  public function T_CachePath($path)
+  {
+    return $this->_CachePath($path);
+  }
+}
+
+class FileCacheBackendTest extends \PHPUnit_Framework_TestCase
+{
+  public function setUp()
+  {
+    $path = dirname(__FILE__) . '/cache/';
+    $this->fixture = new TestFileCacheBackend($path);
+
+    $files = scandir($path);
+    foreach ($files as $file)
+      if ($file[0] != '.')
+        unlink($path . $file);
+  }
+
+  public function testCacheCompiled()
+  {
+    $files = scandir($this->fixture->cache_path());
+    $this->assertEquals(2, count($files));  // Only dotfiles.
+
+    $this->assertNull($this->fixture->GetTemplateDataForName('cache_test', time()));
+
+    $string = 'This is a test.';
+
+    $this->fixture->StoreCompiledTemplate('cache_test', time(), $string);
+    $files = scandir($this->fixture->cache_path());
+    $this->assertEquals(3, count($files));
+
+    $actual = file_get_contents($this->fixture->cache_path() . '/cache_test.phpi');
+    $this->assertEquals($string, $actual);
+  }
+
+  public function testCacheHit()
+  {
+    $path = $this->fixture->cache_path() . '/cache_test.phpi';
+    $expected = 'Cache hit!';
+    file_put_contents($path, $expected);
+
+    $files = scandir($this->fixture->cache_path());
+    $this->assertEquals(3, count($files));
+
+    $actual = $this->fixture->GetTemplateDataForName('cache_test', filemtime($path));
+    $this->assertEquals($expected, $actual);
+  }
+
+  public function testCacheMiss()
+  {
+    file_put_contents($this->fixture->cache_path() . '/cache_test.phpi', 'Invalid template data');
+    $files = scandir($this->fixture->cache_path());
+    $this->assertEquals(3, count($files));
+
+    $actual = $this->fixture->GetTemplateDataForName('cache_test', time() + 60);
+    $this->assertNull($actual);
+  }
+}
index a7844933793de05de53694c2f884df63eb12680f..8dfc66d785ee3bb63d5dd55613c51721425b0514 100644 (file)
@@ -1,11 +1,11 @@
 <?php
 // Hoplite
 // Copyright (c) 2011 Blue Static
-// 
+//
 // This program is free software: you can redistribute it and/or modify it
 // under the terms of the GNU General Public License as published by the Free
 // Software Foundation, either version 3 of the License, or any later version.
-// 
+//
 // This program is distributed in the hope that it will be useful, but WITHOUT
 // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 // FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 namespace hoplite\test;
 use hoplite\views as views;
 
+require_once HOPLITE_ROOT . '/views/cache_backend.php';
 require_once HOPLITE_ROOT . '/views/template_loader.php';
 
-class TestTemplateLoader extends views\TemplateLoader
-{
-  public function T_CachePath($path)
-  {
-    return $this->_CachePath($path);
-  }
-
-  public function T_LoadIfCached($name)
-  {
-      return $this->_LoadIfCached($name);
-  }
-}
-
 class TemplateLoaderTest extends \PHPUnit_Framework_TestCase
 {
   public function setUp()
   {
-    $this->fixture = new TestTemplateLoader();
-
-    $path  = dirname(__FILE__) . '/cache/';
-    $files = scandir($path);
-    foreach ($files as $file)
-      if ($file[0] != '.')
-        unlink($path . $file);
-  }
-
-  protected function _SetUpPaths()
-  {
+    $this->fixture = new views\TemplateLoader();
     $this->fixture->set_template_path(dirname(__FILE__) . '/%s.tpl');
-    $this->fixture->set_cache_path(dirname(__FILE__) . '/cache/');
+
+    $this->cache = $this->getMock('\\hoplite\\views\\CacheBackend');
+    $this->fixture->set_cache_backend($this->cache);
   }
 
   public function testTemplatePath()
   {
-    $this->assertEquals('%s.tpl', $this->fixture->template_path());
-
     $path = '/webapp/views/%s.tpl';
     $this->fixture->set_template_path($path);
     $this->assertEquals($path, $this->fixture->template_path());
   }
 
-  public function testSetCachePath()
-  {
-    $this->assertEquals('/tmp/phalanx_views', $this->fixture->cache_path());
-
-    $path = '/cache/path';
-    $this->fixture->set_cache_path($path);
-    $this->assertEquals($path, $this->fixture->cache_path());
-  }
-
-  public function testCachePath()
-  {
-    $this->fixture->set_cache_path('/test/value/');
-    $this->assertEquals('/test/value/name.phpi', $this->fixture->T_CachePath('name'));
-  }
-
   public function testCacheMiss()
   {
-    $this->_SetUpPaths();
-
-    $files = scandir($this->fixture->cache_path());
-    $this->assertEquals(2, count($files));  // Only dotfiles.
+    $this->cache->expects($this->once())
+                ->method('GetTemplateDataForName')
+                ->with($this->equalTo('cache_test'))
+                ->will($this->returnValue(NULL));
 
-    $this->fixture->Load('cache_test');
+    $expected = file_get_contents(sprintf($this->fixture->template_path(), 'cache_test'));
 
-    $files = scandir($this->fixture->cache_path());
-    $this->assertEquals(3, count($files));
+    $this->cache->expects($this->once())
+                ->method('StoreCompiledTemplate')
+                ->with($this->equalTo('cache_test'),
+                       $this->greaterThan(0),
+                       $this->equalTo($expected));
 
-    $expected = file_get_contents(sprintf($this->fixture->template_path(), 'cache_test'));
-    $actual   = file_get_contents($this->fixture->cache_path() . '/cache_test.phpi');
-    $this->assertEquals($expected, $actual);
+    $this->assertEquals($expected, $this->fixture->Load('cache_test')->template());
+    $this->assertEquals($expected, $this->fixture->Load('cache_test')->template());
   }
 
   public function testCacheHit()
   {
-    $this->_SetUpPaths();
-
     $expected = 'Cache hit!';
-    file_put_contents($this->fixture->cache_path() . '/cache_test.phpi', $expected);
-
-    $files = scandir($this->fixture->cache_path());
-    $this->assertEquals(3, count($files));
-
-    $this->fixture->Load('cache_test');
-
-    $files = scandir($this->fixture->cache_path());
-    $this->assertEquals(3, count($files));
-
-    $actual = file_get_contents($this->fixture->cache_path() . '/cache_test.phpi');
-    $this->assertEquals($expected, $actual);
-  }
-
-  public function testCacheInvalidate()
-  {
-    $this->_SetUpPaths();
-    file_put_contents($this->fixture->cache_path() . '/cache_test.phpi', 'Invalid template data');
-
-    // Need to wait for the mtime to make a difference.
-    sleep(1);
-    clearstatcache();
-
-    // Touch the template to update its mtime.
-    touch(sprintf($this->fixture->template_path(), 'cache_test'));
-
-    $files = scandir($this->fixture->cache_path());
-    $this->assertEquals(3, count($files));
-
-    $this->fixture->Load('cache_test');
-
-    $files = scandir($this->fixture->cache_path());
-    $this->assertEquals(3, count($files));
-
-    $expected = file_get_contents(sprintf($this->fixture->template_path(), 'cache_test'));
-    $actual   = file_get_contents($this->fixture->cache_path() . '/cache_test.phpi');
-    $this->assertEquals($expected, $actual);
+    $this->cache->expects($this->once())
+                ->method('GetTemplateDataForName')
+                ->with($this->equalTo('cache_test'))
+                ->will($this->returnValue($expected));
+
+    // The cache backend is only consulted once.
+    $this->assertEquals($expected, $this->fixture->Load('cache_test')->template());
+    $this->assertEquals($expected, $this->fixture->Load('cache_test')->template());
+    $this->assertEquals($expected, $this->fixture->Load('cache_test')->template());
+    $this->assertEquals($expected, $this->fixture->Load('cache_test')->template());
   }
 
   public function testSingleton()
@@ -144,7 +80,6 @@ class TemplateLoaderTest extends \PHPUnit_Framework_TestCase
     views\TemplateLoader::SetInstance($this->fixture);
     $this->assertSame(views\TemplateLoader::GetInstance(), $this->fixture);
 
-    $this->_SetUpPaths();
     $template = views\TemplateLoader::Fetch('cache_test');
     $this->assertEquals('This file should be cached.', $template->template());
   }
diff --git a/views/cache_backend.php b/views/cache_backend.php
new file mode 100644 (file)
index 0000000..c6fe4a8
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+// Hoplite
+// Copyright (c) 2013 Blue Static
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace hoplite\views;
+
+/*!
+  CacheBackend is used by the TemplateLoader to store and retrieve compiled
+  templates.
+*/
+interface CacheBackend
+{
+  /*!
+    Gets the compiled template data for the template of the given name.
+
+    If the template modification time is newer than what is stored in the cache,
+    this function should discard the item and return NULL.
+
+    @param string The name of the template.
+    @param int The UNIX timestamp the uncompiled template was last modified.
+
+    @return string|NULL The cached template data, or NULL if it is not cached.
+  */
+  public function GetTemplateDataForName($name, $modification_time);
+
+  /*!
+    Stores the compiled template data into the cache with the given name and
+    template modification time.
+
+    @param string The name of the template.
+    @param string The compiled template data.
+    @param int The UNIX timestamp the uncompiled template was last modified.
+  */
+  public function StoreCompiledTemplate($name, $modification_time, $data);
+}
diff --git a/views/file_cache_backend.php b/views/file_cache_backend.php
new file mode 100644 (file)
index 0000000..0a47710
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+// Hoplite
+// Copyright (c) 2013 Blue Static
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace hoplite\views;
+
+require_once HOPLITE_ROOT . '/views/cache_backend.php';
+
+/*!
+  An instance of a CacheBackend that writes cached representations to the file
+  system in temporary files.
+*/
+class FileCacheBackend implements CacheBackend
+{
+  /*! @var string The cache path for templates. This should be a path, to which
+                  the cached template name will be appended. This should not
+                  end with a trailing slash.
+  */
+  protected $cache_path = '/tmp/phalanx_views';
+
+  public function cache_path() { return $this->cache_path; }
+
+  public function __construct($cache_path)
+  {
+    $this->cache_path = $cache_path;
+  }
+
+  public function GetTemplateDataForName($name, $tpl_mtime)
+  {
+    $cache_path = $this->_CachePath($name);
+
+    // Make sure the cached file exists and hasn't gotten out-of-date.
+    if (!file_exists($cache_path) || filemtime($cache_path) < $tpl_mtime)
+      return NULL;
+
+    // Load the contents of the cache.
+    $data = @file_get_contents($cache_path);
+    if ($data === FALSE)
+      return NULL;
+
+    return $data;
+  }
+
+  public function StoreCompiledTemplate($name, $mtime, $data)
+  {
+    $cache_path = $this->_CachePath($name);
+
+    // Cache the file.
+    if (file_put_contents($cache_path, $data) === FALSE)
+      throw new TemplateLoaderException('Could not cache ' . $name . ' to ' . $cache_path);
+  }
+
+  /*! Returns the cache path for a given template name. */
+  protected function _CachePath($name)
+  {
+    return $this->cache_path . $name . '.phpi';
+  }
+}
index f0e624d428ef30dff28d09dca4b43acfb6bf80bb..efa776ab7d286cf0396ccb914dfe1a1050152d92 100644 (file)
@@ -1,11 +1,11 @@
 <?php
 // Hoplite
 // Copyright (c) 2011 Blue Static
-// 
+//
 // This program is free software: you can redistribute it and/or modify it
 // under the terms of the GNU General Public License as published by the Free
 // Software Foundation, either version 3 of the License, or any later version.
-// 
+//
 // This program is distributed in the hope that it will be useful, but WITHOUT
 // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 // FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
@@ -22,7 +22,13 @@ require_once HOPLITE_ROOT . '/base/profiling.php';
 require_once HOPLITE_ROOT . '/views/template.php';
 
 /*!
-  This class knows how to load and cache templates to the file system.
+  The TemplateLoader manages the reading of templates from the file system.
+
+  It can also work with a CacheBackend to store the compiled template data to
+  increase performance.
+
+  Internally it also maintains a cache that it will consult after a template is
+  loaded for the first time.
 */
 class TemplateLoader
 {
@@ -35,16 +41,8 @@ class TemplateLoader
   */
   protected $template_path = '%s.tpl';
 
-  /*! @var string The cache path for templates. Unlike |$template_path|, this
-                  should only be a path, to which the cached template name will
-                  be appended. This should not end with a trailing slash.
-  */
-  protected $cache_path = '/tmp/phalanx_views';
-
-  /*! @var string A header to put at the beginning of each cached template file,
-                  common for setting include paths or cache debug information.
-  */
-  protected $cache_prefix = '';
+  /*! @var CacheBackend A cache for compiled template data. */
+  protected $cache_backend = NULL;
 
   /*! @var array An array of Template objects, keyed by the template name. */
   protected $cache = array();
@@ -68,10 +66,10 @@ class TemplateLoader
 
   /*! Accessors */
   public function set_template_path($path) { $this->template_path = $path; }
-  function template_path() { return $this->template_path; }
-  
-  public function set_cache_path($path) { $this->cache_path = $path; }
-  public function cache_path() { return $this->cache_path; }
+  public function template_path() { return $this->template_path; }
+
+  public function set_cache_backend(CacheBackend $backend) { $this->cache_backend = $backend; }
+  public function cache_backend() { return $this->cache_backend; }
 
   /*!
     Loads a template from a file, creates a Template object, and returns a copy
@@ -90,15 +88,15 @@ class TemplateLoader
     if (isset($this->cache[$name]))
       return clone $this->cache[$name];
 
-    // Then check the filesystem cache.
-    $template = $this->_LoadIfCached($name);
+    // Then check if the cache backend has it.
+    $template = $this->_QueryCache($name);
     if ($template) {
       $this->cache[$name] = $template;
       return clone $template;
     }
 
     // Finally, parse and cache the template.
-    $template = $this->_Cache($name);
+    $template = $this->_Load($name);
     $this->cache[$name] = $template;
     return clone $template;
   }
@@ -119,41 +117,36 @@ class TemplateLoader
   }
 
   /*!
-    Loads a cached filesystem template if it is up-to-date.
+    Queries the optional CacheBackend for a template.
 
     @param string Template name
 
     @return Template|NULL
   */
-  protected function _LoadIfCached($name)
+  protected function _QueryCache($name)
   {
-    $cache_path = $this->_CachePath($name);
-    $tpl_path   = $this->_TemplatePath($name);
-
-    // Make sure the cached file exists and hasn't gotten out-of-date.
-    if (!file_exists($cache_path) || filemtime($cache_path) < filemtime($tpl_path))
+    if (!$this->cache_backend)
       return NULL;
 
-    // Load the contents of the cache.
-    $data = @file_get_contents($cache_path);
-    if ($data === FALSE)
+    $tpl_path = $this->_TemplatePath($name);
+    $data = $this->cache_backend->GetTemplateDataForName($name, filemtime($tpl_path));
+    if (!$data)
       return NULL;
 
     return Template::NewWithCompiledData($name, $data);
   }
 
   /*!
-    Loads a raw template from the file system, stores the compiled template in
-    the file system, and returns a new template object with that data.
+    Loads a raw template from the file system and compiles it. If the optional
+    CacheBackend is present, it will cache the compiled data.
 
     @param string Template name.
 
     @return Template
   */
-  protected function _Cache($name)
+  protected function _Load($name)
   {
-    $cache_path = $this->_CachePath($name);
-    $tpl_path   = $this->_TemplatePath($name);
+    $tpl_path = $this->_TemplatePath($name);
 
     $data = @file_get_contents($tpl_path);
     if ($data === FALSE)
@@ -161,9 +154,10 @@ class TemplateLoader
 
     $template = Template::NewWithData($name, $data);
 
-    // Cache the file.
-    if (file_put_contents($cache_path, $this->cache_prefix . $template->template()) === FALSE)
-      throw new TemplateLoaderException('Could not cache ' . $name . ' to ' . $cache_path);
+    if ($this->cache_backend) {
+      $this->cache_backend->StoreCompiledTemplate(
+          $name, filemtime($tpl_path), $template->template());
+    }
 
     return $template;
   }
@@ -173,12 +167,6 @@ class TemplateLoader
   {
     return sprintf($this->template_path, $name);
   }
-
-  /*! Returns the cache path for a given template name. */
-  protected function _CachePath($name)
-  {
-    return $this->cache_path . $name . '.phpi';
-  }
 }
 
 class TemplateLoaderException extends \Exception {}