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');
...
?>