Append Revision Number to CSS and JS Includes in Symfony
By marc • May 7th, 2008 • Category: SymfonyI’m sure many of us have had a similar experience such as the following. You push out a kick-ass update to a client’s website full of brand new design changes to the main.css file or some funkalicious AJAX features to your javascript files. However, later in the day you get a frantic call from the client and it goes something like this:
Client: “There is a problem with the website!! It looks all messed up! It needs to be fixed right away! We are probably loosing customers!”
You think for a second a realize that their browser must be caching some old version of that main.css file, so it’s not displaying the new design correctly. So the conversation continues:
You: “Oh your browser must have the old stylesheet cached..Just press F5 in your browser.”
Client: “What’s F5? What’s my browser? Oh you mean the Internet Explorer thing?”
I don’t know about you, but this is a conversation that I would like to avoid and never have again for the rest of my life. So luckily the solution is pretty easy in symfony.
The problem:
The problem is pretty simple. Sometimes certain browsers are caching the css or js files for too long, even after they have been updated on the server. So even after the files are updated on the server, visitors who have already visited the website are actually using the old version of the file files that have been cached in their browsers. Naturally this can mess up alot of things if it’s not taken care of.
The basic solution:
In general terms, the solution to this is to let the browser know that there is a new version of the file by changing the file name. In the most basic way you could literally change the filename (main.css to main_1.css), but that’s obviously not the best way to do it. The better way to do it is by appending a version or revision number to the end of the file name as a parameter. So you would have something like this in your page head:
-
-
<script type="text/javascript" src="common.js?v=20080423"></script>
-
<link rel="stylesheet" type="text/css" media="screen" href="main.css?v=20080423" />
In the example above, I am just appending the current date to the end of the file. So the basic idea is that your are putting the revision date of a code push to all of the static files that get included into your main layout file.
The symfony solution:
Now there is a way that we can change around a few things in our symfony project to make this change easy in a DRY (don’t repeat yourself) fashion.
The first step is to override the AssetHelper.php file that is within the symfony core files. All you have to do is copy this file from {SYMFONY_LIB_DIR}/helper/ to {YOUR_PROJECT_DIR}/lib/helper/AssetHelper.php.
Now you just need to add two small changes to this helper file. We are just going to add a constant called REVISION along with a parameter (?v=) and append them to the file names.
First change the get_javascripts() function so that it looks like the following:
-
-
function get_javascripts()
-
{
-
$response = sfContext::getInstance()->getResponse();
-
$response->setParameter('javascripts_included', true, 'symfony/view/asset');
-
-
$already_seen = array();
-
$html = '';
-
-
foreach (array('first', '', 'last') as $position)
-
{
-
foreach ($response->getJavascripts($position) as $files)
-
{
-
if (!is_array($files))
-
{
-
$files = array($files);
-
}
-
-
foreach ($files as $file)
-
{
-
$file = javascript_path($file) . "?v=" . REVISION; // our revision addition
-
-
if (isset($already_seen[$file])) continue;
-
-
$already_seen[$file] = 1;
-
$html .= javascript_include_tag($file);
-
}
-
}
-
}
-
-
return $html;
-
}
Secondly, change the get_stylesheets() function so that it looks like this:
-
-
function get_stylesheets()
-
{
-
$response = sfContext::getInstance()->getResponse();
-
$response->setParameter('stylesheets_included', true, 'symfony/view/asset');
-
-
$already_seen = array();
-
$html = '';
-
-
foreach (array('first', '', 'last') as $position)
-
{
-
foreach ($response->getStylesheets($position) as $files => $options)
-
{
-
if (!is_array($files))
-
{
-
$files = array($files);
-
}
-
-
foreach ($files as $file)
-
{
-
$file = stylesheet_path($file) . "?v=" . REVISION; // our revision addition
-
-
if (isset($already_seen[$file])) continue;
-
-
$already_seen[$file] = 1;
-
$html .= stylesheet_tag($file, $options);
-
}
-
}
-
}
-
-
return $html;
-
}
Finally you just need to define that REVISION constant somewhere. It’s up to you where to put it. But for simplicity sake, let’s just say that you put it in the front controller for your frontend application (/web/index.php). You would have something like this in the beginning of the file:
-
-
// set the revision global variable
-
define('REVISION', '2008042300');
A better solution would probably be to create a system that stores the revision number in a text file that gets included somewhere in your application. Then this file can be automatically updated through your application when you make updates. Another possible approach would be to update the file through a build tool such as Ant, or to extend the symfony sync task so that it automatically updates when you do a code push.