PHP Smarty Tutorial: Caching and Versioning Static Content

I am a nascent advocate of using Smarty as a view templating engine in PHP. Some—including myself until recently—might ask “Why use a templating engine for PHP, which is a templating engine?” In this post I hope to give you one example of how Smarty can clean up code in a useful way.

Static Content

Static content refers to primarily images, Cascading Style Sheets (“CSS”) and JavaScript files but can also include such things as videos, audio clips and so on. All of this tends to be labelled “static content” as it is largely unchanging (as opposed to Web pages, which are largely dynamic).

Static content can include things like static HTML files but in practice other methods tend to be more common for caching HTML content.

Caching

Static content can be cached in two places: on the client and on the server.

Caching on the server isn’t typically an issue with truly static content. It’s more an issue with cached content that is generated in some way, such as the dynamically generated Javascript files from Supercharging Javascript in PHP. Serverside caching is not the focus of this post.

Client caching involves telling the browser to keep a copy of certain content and not request it from the server. There are several variations upon this theme:

  1. Caching for a fixed period of time. For example, setting the HTTP Expires header in the future;
  2. Allowing the browser to ask if the content has changed. For example, use of ETags in HTTP/1.1; and
  3. Some combination of the two.

The one we’ll focus on is (1). It is a simple scheme but you must balance setting the Expires header sufficiently far in the future versus making spurious requests to the server when the content hasn’t changed.

Versioning

As a solution to the far futures Expires header problem of content that changes, a common scheme used by modern Websites is to use some form of versioning. What this means is this: the Expires header is set in the far future (one or more years). When the content changes a new URL is generated. This forces the browser to reload the content.

In my previous articles I used URLs like this:

http://example.com/images/logo.1233454569.png

The disadvantage of this is that it requires URL rewriting to work. I have since settled on a simpler scheme: putting the file’s last modified time as the query string. This means a URL like this:

http://example.com/images/logo.png?1233454569

No URL rewriting required and it should work out of the box with Web servers.

Static Content Domain

Another optimization that is encouraged is to split your publicly available static content onto a separate domain. The reason is that you could minimize traffic and overhead by the client not sending unnecessary cookies.

Image Dimensions

Lastly, images in particular can be optimized by specifying their height and width in the HTML. This can speed up how fast the browser renders the page and whether or not there are possibly unsightly visual artefacts as the page loads, such as content jittering or shifting position while it is loading.

Directory Structure

This example will assuming the following directory structure:

/var/www                    Top-level on host
+- /site                    Document root of dynamic domain
+- /static                  Document root of static domain
   +- /images               Images subdirectory
+- /lib                     Third-party code
   +- /Smarty-2.6.26        Smarty install files
+- /include                 PHP files not served by the Web server and thus not under the document root
   +- Mysmarty.class.php    Custom Smarty template generation. See Smarty installation notes.
   +- /templates            Smarty templates
   +- /templates_c          Compiled Smarty templates
   +- /cache                Cached Smarty content
   +- /config               Smarty config files
   +- /plugins              Smarty plugins

Your structure may of course vary. I will say I prefer to put nothing under the document root that isn’t the endpoint of an HTTP request.

The Apache, IIS, nginx or other config to set the far future Expires header is assumed.

Mysmarty.class.php

A common technique with Smarty is to create a custom subclass of Smarty that sets all the correct configuration. This looks something like this:

<?php
define('BASE_DIR',      '/var/www/');
define('LIB_DIR',       BASE_DIR . 'lib/');
define('INCLUDE_DIR',   BASE_DIR . 'include/');

// separate static site
define('STATIC_URL',    'http://mystatic.com/');
define('STATIC_DIR',    BASE_DIR . 'static/');

// use the following if everything is on one site
//define('STATIC_URL',    '/');
//define('STATIC_DIR',    BASE_DIR . 'site/');

// Smarty directories
define('TEMPLATES_DIR', INCLUDE_DIR . 'templates/');
define('CONFIG_DIR',    INCLUDE_DIR . 'config/');
define('COMPILE_DIR',   INCLUDE_DIR . 'templates_c/');
define('CACHE_DIR',     INCLUDE_DIR . 'cache/');
define('PLUGINS_DIR',   INCLUDE_DIR . 'plugins/');

// static content
define('IMAGES_DIR',    STATIC_DIR . 'images/');

// URLs
define('IMAGES_URL',    STATIC_URL . 'images/');

require '../lib/Smarty-2.6.26/libs/Smarty.class.php';

class MySmarty extends Smarty {
  function MySmarty() {
    $this->Smarty();

    $this->template_dir = TEMPLATES_DIR;
    $this->compile_dir  = COMPILE_DIR;
    $this->config_dir   = CONFIG_DIR;
    $this->cache_dir    = CACHE_DIR;
    $this->plugins_dir  = PLUGINS_DIR;
  }
}
?>

Hopefully you should be able to adapt the above to whatever directory structure you use.

Smarty Image Plugin

To demonstrate this, I’ll use just one example: images. You can however easily apply this to other forms of static content.

Assume the following code exists and is accessible to the plugin:

<?php
$standard_attributes = array('class', 'dir', 'id', 'lang', 'style', 'title');

function standard_attributes(array $attributes) {
  global $standard_attributes;
  $ret = '';
  foreach ($standard_attributes as $attr) {
    if (isset($attributes[$attr])) {
      $ret .= attribute($attr, $attributes[$attr]);
    }
  }
  return $ret;
}

function attribute($name, $value) {
  return ' ' . $name . '"' . htmlspecialchars($value) . '"';
}

function build_image($params, &$smarty) {
  $src = $params['src'];
  if (!isset($src)) {
    return "[No src to image]";
  }
  $file = IMAGES_DIR . $src;
  $mtime = filemtime($file);
  if ($mtime === false) {
    return "[Image '$src' not found]";
  }
  $height = $params['height'];
  if (!isset($height)) {
    $size = getimagesize($file);
    $height = $size[1];
  }
  $width = $params['width'];
  if (!isset($width)) {
    if (!isset($size)) {
      $size = getimagesize($file);
    }
    $width = $size[0];
  }
  $alt = htmlspecialchars($params['alt']);
  $attribs = standard_attributes($params);
  $url = IMAGES_URL . htmlspecialchars($src);
  return <<<END
<img alt="$alt" src="$url?$mtime" width="$width" height="$height" $attribs>
END;
}
?>

It makes sense to put the above into Mysmarty.class.php or at least include/require it there.

In the Smarty plugins directory, you need to create a file called function.image.php.

<?php
function smarty_function_image($params, &$smarty) {
  return build_image($params, &$smarty);
}
?>

You may well ask "why not put the code in the plugin directly instead of calling a function?" and you'd be right except for one thing: I will reuse it for another plugin but more on that later.

This plugin does several things:

  1. It verifies that the specified image actually exists in the expected location;
  2. It gets the last modified time (mtime) of the image file;
  3. It prepends the correct URI to the image name including the static domain if there is one;
  4. The plugin user can specify as many or as few attributes of the image tag as they wish. This version is written to handle most that you would expect to encounter and is easily extensible to include others;
  5. It uses the GD library to get the height and width of the image, if required. It is only required if the user does not specify both. The user-supplied values are used in preference.

That’s a lot. In a template it is nothing more than:

{image src="logo.gif"}
{image src="top.png" height="100" width="300" alt="Some alt text"}
etc

You can of course do that in standard PHP. I've previously created functions like auto_version() just for this purpose but it's nowhere near as readable and maintainable. The pure PHP version would have to pass in an array of parameters like so:

<?php echo auto_version(array('src' => 'logo.gif', 'height' => '100', 'width' => '200', 'alt' => 'Some alt test'); ?>

But it gets even better.

Compiler Plugins

The astute reader will point out the inefficiency of this: every page request will access the image file once or twice. Firstly to get the mtime. Secondly to get the height and width (if not specified by the user). What’s more the second operation is arguably more expensive since it necessitates reading in the image file and processing it.

The plugin we just created is, in Smarty terms, a template function. The function is called with every view of the template. An alternative is to use a compiler function.

The basic lifecycle of a template is:

  1. If the template file is newer than the compiled template or there is no compiled template, compile it;
  2. Templates are compiled into PHP files that are put in the ‘compile_dir’ directory (typically ‘templates_c’);
  3. Caching and other config can change this.

A template function is called on each execution. A compiler function is only called when the template is compiled into PHP.

You might say that for many of your images they change very infrequently. As such the overhead of reading the dimensions and the last modified time is overkill. This is a good candidate for a compiler function. These values will only be calculated once.

The downside is that if the image does change you will need to regenerate the compiled templates. This is easily done by simply deleting the relevant contents of the compile directory.

Static Image Plugin

We will create another version of our plugin that will be executed at template compile time. We will call this one ‘static_image’. If it had the same name ('”image”) as the template function, the compiler function would take precedence (ie the other would not be called).

There are several differences with template functions:

  1. The file is named compiler.static_image.php;
  2. The function is named smarty_compiler_static_image();
  3. The raw argument (eg ‘src=”logo.png height=”100”’) is passed as a string to the plugin instead of an array of key-value pairs; and
  4. A compiler function generates PHP statements instead of HTML markup.

Bearing all this in mind, we need two functions: one to parse the raw text argument and another to generate the PHP statements. Also, the build_image() function needs to handle a string parameter.

_parse_attrs($tag_attrs);
  $ret = array();
  foreach ($args as $k => $v) {
    $ret[$k] = $smarty->_dequote($v);
  }
  return $ret;
}

function php_echo(string $text) {
  return 'echo "' . addslashes($text) . "\";";
}

function build_image($params, &$smarty) {
  if (!is_array($params)) {
    $params = build_params($params);
  }
  $src = $params['src'];
  if (!isset($src)) {
    return "[No src to image]";
  }
  $file = IMAGES_DIR . $src;
  $mtime = filemtime($file);
  if ($mtime === false) {
    return "[Image '$src' not found]";
  }
  $height = $params['height'];
  if (!isset($height)) {
    $size = getimagesize($file);
    $height = $size[1];
  }
  $width = $params['width'];
  if (!isset($width)) {
    if (!isset($size)) {
      $size = getimagesize($file);
    }
    $width = $size[0];
  }
  $alt = htmlspecialchars($params['alt']);
  $attribs = standard_attributes($params);
  $url = IMAGES_URL . htmlspecialchars($src);
  return <<<END
<img alt="$alt" src="$url?$mtime" width="$width" height="$height" $attribs>
END;
}
?>

With these changes and additions, the compiler template becomes:

<?php
function smarty_compiler_static_image($tag_attrs, &$smarty) {
  return php_echo(build_image($tag_attrs, &$smarty));
}
?>

Using it is as simple as:

{static_image src="logo.png"}

This is something not easily done in pure PHP.

Conclusion

This is but scratching the surface of what Smarty is capable of but I hope it provides a useful demonstration of what makes Amsarty so good.

What’s more this functionality comes at very low cost since Smarty is simply compiled into straight PHP. Barring the initial compile, the only cost on a per-request basis is one to see if the template has changed (and thus needs a recompile). In production environments this check can even be disabled for further performance.

10 comments:

Sean said...

Or you could avoid the mess that is 'Smart'y and write a small caching function to cache the output of your views (you are using views right?).

Anonymous said...

@Sean your (dense) insight is truly inspiring.

Sean said...

@Anonymous:
It is really only three steps, here, I will break it down for you:

1: Finish loading from the database
2: buffer output
3: save it to a timestamped file.

Next time the page loads, check to see if the content is X minutes old. If it is greater than X load it from the database again. No need to use 'Smart'y or any other template language on top of another template language.

Anonymous said...

Sure there is. Smarty modifiers and simple plugins like xsl are reason enough. We've all met people like Sean. They love outputting the php open/close tags everywhere and wouldn't call it a good day unless they've done string concatenation to output some html.

Anonymous said...

sorry but Smarty is a mess. "Oh, but you can use any php function in smarty templates they just use different names and syntax..." i rest my case. Good for caching and select boxes otherwise use php.
For great examples of php based templates see Symfony framework.

youtubeline said...

smarty is flexible for pages and useful difference templates using. tutorial youtube

discount said...

thank you sir

Php Programmer said...

my point of view smarty is for designers who dont know server scripting language and must "communicate" with the developer to complete the project.

Anonymous said...

hi.

Raj malhothra said...

Step 5: Grant write permissions to cache/templates (0777 or 0755)
-what i have to do for this, im anewbe in smarty pls help me to get stand

Post a Comment