You are currently viewing the personal blog of Wiras Adi, a web designer and application developer, located in Jakarta, Indonesia.

Invalidate Smarty Cache Using Cache Dependency Files

Smarty is perhaps the most powerful and widely used PHP template engine available for PHP-based application developments. Though its usage has now been a bit pulled-aside by the rise of more advance frameworks such as Drupal or CakePHP, which come with their own template system implementations, for a “bare” template engine Smarty is still the favorite. I myself still use the combination of Smarty and PEAR (PHP Extension and Application Repository) for most of my PHP projects.

Taking it more than just a template engine, Smarty provides a built-in caching functionality. Smarty caches the server response, and that is in pure HTML. This is nice in the term of performance, especially for an extensive script-processing generated page (ie. script that does several database queries). As long as the cache hasn’t been invalidated, in subsequent request of the page Smarty will simply return the pure HTML cache instead of executing the script every time.

Smarty supports time-based cache dependency, meaning that you determine how long Smarty holds the cache before the page will be regenerated. You do this by setting the $cache_lifetime Smarty class variable.

// Turn on the caching functionality
$smarty->caching = 1;
// Determine cache lifetime (in seconds)
$smarty->cache_lifetime = 3600;

The above code simply tells Smarty to keep the cache of page for an hour before the page regenerated. Although this method will be sufficient for most situations, it’s not powerful enough for pages that contains/depends on critical data. It is not suitable if your pages need to refresh immediately after the underlying data changes. One solution to overcome this problem is to use file-based cache dependency.

How it works?

This method works basically by associating Smarty template with one or more files, modifications to any of these files will invalidate the cache of that template. Smarty will compare the modification time with the cache creation time; if at least one file has newer modification time compared to the cache creation time then Smarty will invalidate the cache, regenerate the template and create a new cache.

The content of dependency files is not important, as we need only its name and modification time. An empty (zero-byte) file should be enough to act as dependency file.

File modification time can easily be updated using PHP function touch. You call this touch the dependency files routine whenever you update the underlying data, whether it be in your admin pages or in any places where you might update the data. You may also want to consider using database trigger to touch the files. It is really up to you how you update the modification time of the dependency files appropriately concerning your application.

Implementation

To implement this functionality, I will simply extend the Smarty class and override the is_cached method. I hope comments inside the code would make the concept clear enough to grasp. For your convenient, you can download a copy of the file instead of rewrite it yourself.

<?php
// Include Smarty class file
require_once('Smarty/libs/Smarty.class.php');

// New class extended from Smarty
class Smarty_File_Cache_Deps extends Smarty {
  // Repository directory for dependency files
  var $cachedep_dir;
  // Constructor
  function Smarty_File_Cache_Deps {
    // Call parent constructor
    parent::Smarty();
    // Set Smarty working directories plus the one for dependency files directory
    $this->template_dir = DATA_DIR . 'templates/';
    $this->compile_dir = DATA_DIR . 'templates_c/';
    $this->cache_dir = DATA_DIR . 'cache/';
    $this->config_dir = DATA_DIR . 'configs/';
    $this->cachedep_dir = DATA_DIR . 'cache_deps/';
  }

  // Use this method instead of Smarty's is_cached()
  // This method will also check whether the dependency file(s)
  // that the cache depend on has changed.
  // @params  string  $tpl_file  the Smarty template file
  // @params  mixed  $dep_file  the dependency file(s)
  //  which the cache depend on (I added this one), string or array of files
  // @params  mixed  $cache_id  the cache id you specified
  // @params  mixed  $compile_id  the compile id you specified
  // @return  bool
  function is_cached_respect_deps($tpl_file, $dep_file, $cache_id = null, $compile_id = null) {
    // Call the parent is_cached method first
    if ($this->is_cached($tpl_file, $cache_id, $compile_id)) {
      // Clear PHP stat cache
      clearstatcache();
      // Check the file(s), using validate_dependency() method for each file
      if (is_array($dep_file)) {
        foreach($dep_file as $value) {
          if (!$this->validate_dependency($value)) return false;
        }
      } else {
        if (!$this->validate_dependency($dep_file)) return false;
      }
    } else {
      return false;
    }
    return true;
  }

  // Private method that actually do the checking
  function validate_dependency($dep_file) {
    if (file_exists($this->cachedep_dir . $dep_file)) {
       // Get modification time
      $dep_time = filemtime($this->cachedep_dir . $dep_file);
      // Return false if the modification time of the file is newer than the cache creation time
      if ($dep_time) {
        if ((double)$dep_time > (double)$this->_cache_info['timestamp']) return false;
      } else {
        $this->trigger_error('unable to read cache dependency file \'' . $this->cachedep_dir . $dep_file . '\'.');
        return false;
      }
    } else {
      $this->trigger_error('cache dependency file \'' . $this->cachedep_dir . $dep_file . '\' doesn\'t  exist.');
      return false;
    }
    return true;
    }
}
?>

Then in your page you would write something like this:

<?php
...
$smarty = new Smarty_File_Cache_Deps();
$smarty->caching = 1;
// This is the way you associate sometemplate.tpl template with 3 dependency files, namely
// dep1.dep, dep2.dep, dep3.dep
if (!$smarty->is_cached_respect_deps('cms/sometemplate.tpl', array('dep1.dep', 'dep2.dep', 'dep3.dep'))) {
  // Do whatever to (re)generate the page here
}
$smarty->display('cms/sometemplate.tpl');
...
?>
 

5 responses to this entry so far.

  1. Antonio Bueno

    I’m considering Smarty for my site and found your post when looking for information on Smarty caching. I like your file-based idea although I find it a bit restrictive (I miss including URL parameters, for example).

    But wouldn’t this be a particular case of “multiple caches”?
    http://www.smarty.net/manual/en/caching.multiple.caches.php

    To replicate your solution I would combine file names and timestamps in a single “cache_id” (maybe a hash).

    #1
  2. Wiras Adi

    Hello Antonio,

    Actually, the technique I wrote in this post is merely intended to be a mechanism to refresh the cache at precisely when the page need to be refreshed. With cache_lifetime Smarty currently provides, cached page will refresh only when it reaches its lifetime in the pool. Thus there’s a possibility that the page doesn’t display a very actual data when some data changes happened within the lifetime of the cache. The technique tries to make the cache dependent to external events to trigger the refreshing.

    On the other hand, multiple caching is a way to display different versions of a page conditionally, where one page has multiple displays depending on some parameters. With multiple caching, Smarty can cache all possible displays. But it still lack a kind of “real-time” cache expiration support. And this file-based cache dependency technique can also be applied in conjunction multiple caching, in a similar way as in ordinary caching mechanism.

    About your idea to “inject” the file names and timestamps information in cache_id, don’t you think that it would require relatively much works tweaking the Smarty system instead of just extend the class?

    I haven’t tried it yet, but I really think that it’s a nice idea as it might lead to a cleaner application design than the one I proposed here.

    #2
  3. Andrew Toan

    Hello, Wiras
    Thanks for your post.
    I’m really like your file-based cache dependency based on Smarty. But i have some questions i still don’t understant. Can u make it clearer for me?

    In the even, i update the data in my database and my system has a lot of template file (*.tpl). How can i make the Smarty know which template or which cache need to regenerate again. I just has an idea: each template file i also have a file-based dependency and whenever i update the specific data, i also update the modification time of dependent file.

    Hope u reply soon

    #3
  4. Wiras Adi

    Hello Andrew,

    Yes, that’s how it works, you need to update the modification time of the dependency files every time you make changes to the underlying data (in PHP you can use the built-in touch() function).

    About your question “how to make Smarty knows which cached files in the pool needs to be refreshed when particular underlying data changed?”, the first thing crosses my mind is to implement a kind of naming conventions technique in how you make the dependency files. Say, you have a certain template that dependent to rows of data who have, say, “Japan” as the value of its country column. Then you might want to name the dependency files for that template something like “dep_file_japan.dep”, another_dep_file_japan.dep”, etc. Using a simple Regex checking, you could easily touch() all the dependency files related to that specific data and template.

    As an alternative you can also use a kind of configuration settings (using database or just configuration files) in order to “map” template files to its underlying data. But this could unnecessarily complicate things as your data grows.

    I hope I’ve made myself clear with that explanation there. If you have a better solution I’d be glad if you like to share it here.

    #4
  5. 7u15itq5pl

    twufo72 rKHyq7 clvkd7 firRQ15 howai19 xpKpo65 NHjoj0 yhRVc54

    #5