Irrational Exuberance!

Several Approaches to Versioning Files in PHP

July 28, 2009. Filed under php

In a project at work I am in the process of putting some finishing touches on the frontend, and ran into a bit of a dilemna last week: how to use versioned static files with PHP with a minimum of work?


If you haven't used versioned static files before, it's a rather useful trick. You can configure Apache/Lighttpd/Nginx to inform browsers that a given file (or contents of a directory, or files with certain extensions, etc) won't change in the next 5 minutes, 3 days, or even 10 years. This serves as a strong suggestion to the browser to cache the specified file and not attempt to reload it until the specified time. This lowers your bandwidth (serve fewer files), reduces server load (fewer http requests per pageview), and makes page load faster (another effect of having fewer http requests per pageview).

An awkward side-effect is that users won't reload those files after you make changes unless they force-refresh or clear their browser cache (neither of which happen with great regularity). The standard solution to this is to add a version to the filename, i.e. styles.css becomes styles-1.css, and later styles-231.css. Returning to the problem, how should we add that version number to our files without needing to manually update our code with the newest file versions?


I see two general solutions to this problem, regardless of which we go with the usage is going to look the same,

<?php
$f = get_versioned_filename('styles', 'css');
echo "<link rel='stylesheet' type='text/css' href='$f' title='EDoc'>";
?>

but the underlying mechanism will be a bit different.

Compile Time Version Discovery

Although much of the web is built on interpreted languages (PHP, CSS, JS, HTML), many projects nonetheless end up requiring a compile stage. For static files like CSS and JS that is typically sending the files through a compressor of some kind (the examples on this page will send the files through YUI Compressor).

Compilation gives a nice leverage point for assigning all the files the same version number, and then passing that context along to the server.

HERE=`pwd`
PROJECT="$HERE/project"
BUILD="$HERE/build"
# download YUI from the link above
YUI="$HERE/tools/yuicompressor-2.4.2.jar"
rm -rf $BUILD
mkdir "$BUILD"

TS=`date +%s`
for dir in "$PROJECT/css *.css $BUILD/css" "$PROJECT/js *js $BUILD/js"
do
    set -- $dir
    cd $1
    mkdir $3
    for f in $2
    do
        echo "Processing $f ..."
        java -jar $YUI $f > $3/${f%.*}.$TS.${f#*.}
    done
done
mkdir $BUILD/php
echo "project_files_version = $TS" >> $BUILD/php/versioned_files.ini

Then in your project's installation script you'll need to do something along the lines of

cp build/css/ /path/to/webserver/css/
cp build/js/ /path/to/webserver/js/
cp build/php/ path/to/php/config/include/directory/
sudo apache2ctl graceful

Note that it is necessary to restart Apache in order to reload the config file.

After running the build script, we just need to write this function for generating the full pathname.

<?php
function get_versioned_filename($base, $ext) {
  return $base .'.'. get_cfg_var('versioned_files') .'.' . $ext;
}
?>

All in all, this is a workable solution, but fairly awkward. Can we do better?

Run Time Version Discovery

Another solution would be to scan the directory to discover the name of the file (you'll still need to use the same compile script above, but without the config file, and thus without the requirement of restarting your server after each change).

<?php
function get_versioned_filename($base, $ext) {
    // directory is some/path/js for .js files, and 
    // some/path/css for .css files
    if($handle = opendir("/path/to/webserver/$ext")) {
        while (false !== ($file = readdir($handle))) {
            if (preg_match("/$base\.\d+\.$ext/", $file)) {
                closedir($handle);
                return $file;
            }
        }
    }
    closedir($handle);
    return false;
}
?>

If we were really going to use the above code, we would probably want to extend it to show preference for higher version numbers, which would make it is possible for (likely, actually, depending on how you are deploying your app) two version of a given file to cohabitant.

Another likely improvement would be to cache the versioned filenames at some level (perhaps using the Alternative PHP Cache), but doing so adds a layer of complexity when you want to update to a newer version of the filenames, which in the would likely end up being more complex than simply restarting Apache to reload the newest config settings.

Better Suggestions?

In the end, I'm not particularly pleased with either of these solutions, although they are both workable. My PHP background isn't particularly deep, so there may be a standard PHP solution to this problem that I have simply overlooked.

Any suggestions?

(As a side note, I included the <?php ?> tags because Pygments seems to require them for properly highlighting PHP snippets.)