Generating CSS Files Dynamically Using PHP Scripts?

Hi @Ash G:

I didn’t follow 100% where you were specifically having problems so I’m not sure I really can answer your issues point-by-point but I can explain how to do this from ground up. And unless what you are doing is a good bit more involved then you mentioned it’s a little bit more work than I think you were anticipating but it is still completely doable. And even if I’m covering lots of ground that you already know there’s a good chance other’s with less knowledge or experience will find this via Google and be helped by it too.

Bootstrap WordPress with /wp-load.php

The first thing we need to do in your my_theme_css.php file is bootstrap WordPress’ core library functions. The following line of code loads /wp-load.php. It uses $_SERVER['DOCUMENT_ROOT'] to locate the website’s root so you don’t have to worry about where you store this file on your server; assuming DOCUMENT_ROOT is set correctly as it always should be for WordPress then this will bootstrap WordPress:

<?php
include_once("{$_SERVER['DOCUMENT_ROOT']}/wp-load.php"); 

So that’s the easy part. Next comes the tricky part…

PHP Scripts Must Handle All Caching Logic

Here’s where I’ll bet you might have stumbled because I sure did as I was trying to figure out how to answer your question. We are so used to the caching details being handled by the Apache web server that we forget or even don’t realize we have to do all the heavy lifting ourselves when we load CSS or JS with PHP.

Sure the expiry header may be all we need when we have a proxy in the middle but if the request makes it to the web server and the PHP script just willy-nilly returns content and the “Ok” status code well in essence you’ll have no caching.

Returning “200 Ok” or “304 Not Modified”

More specifically our PHP file that returns CSS needs to respond correctly to the request headers sent by the browser. Our PHP script needs to return the proper status code based upon what those headers contain. If the content needs to be served because it’s a first time request or because the content has expired the PHP script should generate all the CSS content and return with a “200 Ok”.

On the other hand if we determine based on the cache-related request headers that the client browser already has the latest CSS we should not return any CSS and instead return with a “304 Not Modified”. The too lines of code for this are respectively (of course you’d never use them both one line after another, I’m only showing that way here for convenience):

<?php 
header('HTTP/1.1 200 Ok');
header('HTTP/1.1 304 Not Modified');

Four Flavors of HTTP Caching

Next we need to look at the different ways HTTP can handle caching. The first is what you mention; Expires:

  • Expires: This Expires header expectz a date in the (PHP gmdate() function) format of 'D, d M Y H:i:s' with a ' GMT' appended (GMT stands for Greenwich Mean Time.) Theoretically if this header is served the browser and downstream proxies will cache until the specified time passes after which it will start requesting the page again. This is probably the best known of caching headers but evidently not the preferred one to use; Cache-Control being the better one. Interestingly in my testing on localhost with Safari 5.0 on Mac OS X I was never able to get the browser to respect the Expires header; it always requested the file again (if someone can explain this I’d be grateful.) Here’s the example you gave from above:

header("Expires: Thu, 31 Dec 2020 20:00:00 GMT");

  • Cache-Control: The Cache-Control header is easier to work with than the Expires header because you only need to specify the number of time in seconds as max-age meaning you don’t have to come up with an exact date format in string form that is easy to get wrong. Additionally Cache-Control allows several other options such as being able to tell the client to always re-validated the cache using the mustrevalidate option and public when you want to force caching for normally non-cachable requests (i.e requests via HTTPS) and even not cache if that’s what you need (i.e. you might want to force a a 1×1 pixel ad tracking .GIF not to be cached.) Like Expires I was also unable to get this to work in testing (any help?) The following example caches for a 24 hour period (60 seconds by 60 minutes by 24 hours):

header("Cache-Control: max-age=".60*60*24.", public, must-revalidate");

  • Last-Modified / If-Modified-Since: Then there is the Last-Modified response header and If-Modified-Since request header pair. These also use the same GMT date format that the Expires header use but they do a handshake between client and server. The PHP script needs to send a Last-Modified header (which, by the way, you should update only when your user last updates their custom CSS) after which the browser will continue to send the same value back as an If-Modified-Since header and it’s the PHP script’s responsibility to compare the saved value with the one sent by the browser. Here is where the PHP script needs to make the decision between serving a 200 Ok or a 304 Not Modified. Here’s an example of serving the Last-Modified header using the current time (which is not what we want to do; see the later example for what we actually need):

header("Last-Modified: " . gmdate('D, d M Y H:i:s', time()).'GMT');

And here is how you’d read the Last-Modified returned by the browser via the If-Modified-Since header:

$last_modified_to_compare = $_SERVER['HTTP_IF_MODIFIED_SINCE'];

  • ETag / If-None-Match: And lastly there’s the ETag response header and the If-None-Match request header pair. The ETag which is really just a token that our PHP sets it to a unique value (typically based on the current date) and sends to the browser and the browser returns it. It the current value is different from what the browser returns your PHP script should regenerate the content an server 200 Ok otherwise generate nothing and serve a 304 Not Modified. Here’s an example of setting an ETag using the current time:

header("ETag: " . md5(gmdate('D, d M Y H:i:s', time()).'GMT'));

And here is how you’d read the ETag returned by the browser via the If-None-Match header:

$etag_to_match = $_SERVER['HTTP_IF_NONE_MATCH'];

Now that we’ve covered all that let’s look at the actual code we need:

Serving the CSS file via init and wp_enqueue_style()

You didn’t show this but I figured I would show it for the benefit of others. Here’s the function call that tells WordPress to use my_theme_css.php for it’s CSS. This can be stored in the theme’s functions.php file or even in a plugin if desired:

<?php
add_action('init','add_php_powered_css');
function add_php_powered_css() {
  if (!is_admin()) {
    $version = get_theme_mod('my_custom_css_version',"1.00");
    $ss_dir = get_stylesheet_directory_uri();
    wp_enqueue_style('php-powered-css', 
        "{$ss_dir}/my_theme_css.php",array(),$version);
  }
}

There are several points to note:

  • Use of is_admin() to avoid loading the CSS while in the admin (unless you want that…),
  • Use of get_theme_mod() to load the CSS with a default version of 1.00 (more on that in a bit),
  • Use of get_stylesheet_directory_uri() to grab the correct directory for the current theme, even if the current theme is a child theme,
  • Use of wp_enqueue_style() to queue the CSS to allow WordPress to load it at the proper time where 'php-powered-css' is an arbitrary name to reference as a dependency later (if needed), and the empty array() means this CSS has no dependencies (although in real world it would often have one or more), and
  • Use of $version; Probably the most important one, we are telling wp_enqueue_style() to add a ?ver=1.00 parameter to the /my_theme_css.php URL so that if the version changes the browser will view it as a completely different URL (Much more on that in a bit.)

Setting $version and Last-Modified when User Updates CSS

So here’s the trick. Every time the user updates their CSS you want to serve the content and not wait until 2020 for everyone’s browser cache to timeout, right? Here’s a function that combined with my other code will accomplish that. Every time you store CSS updated by the user, use this function or functionality similar to what’s contained within:

<?php
function set_my_custom_css($custom_css) {
  $new_version = round(get_theme_mod('my_custom_css_version','1.00',2))+0.01;
  set_theme_mod('my_custom_css_version',$new_version);
  set_theme_mod('my_custom_css_last_modified',gmdate('D, d M Y H:i:s',time()).' GMT');
  set_theme_mod('my_custom_css',$custom_css);
}

The set_my_custom_css() function automatically increments the current version by 0.01 (which was just an arbitrary increment value I picked) and it also sets the last modified date to right now and finally stores the new custom CSS. To call this function it might be as simple as this (where new_custom_css would likely get assigned via a user submitted $_POST instead of by hardcoding as you see here):

<?php
$new_custom_css="body {background-color:orange;}";
set_my_custom_css($new_custom_css);

Which brings us to the final albeit significant step:

Generating the CSS from the PHP Script

Finally we get to see the meat, the actual my_theme_css.php file. At a high level it tests both the If-Modifed-Since against the saved Last-Modified value and the If-None-Match against the ETag which was derived from the saved Last-Modified value and if neither have changed just sets the header to 304 Not Modifed and branches to the end.

If however either of those have changed it generates the Expires, Cache-Control. Last-Modified and Etag headers as well as a 200 Ok and indicating that the content type is text/css. We probably don’t need all those but given how finicky caching can be with different browsers and proxies I figure it doesn’t hurt to cover all bases. (And anyone with more experience with HTTP caching and WordPress please do chime in if I got any nuances wrong.)

There are a few more details in the following code but I think you can probably work them out on your own:

<?php

  $s = $_SERVER;

  include_once("{$s['DOCUMENT_ROOT']}/wp-load.php");

  $max_age = 60*60*24; // 24 hours
  $now = gmdate('D, d M Y H:i:s', time()).'GMT';
  $last_modified = get_theme_mod('my_custom_css_last_modified',$now);
  $etag = md5($last_modified);

  if (strtotime($s['HTTP_IF_MODIFIED_SINCE']) >= strtotime($last_modified) || $s['HTTP_IF_NONE_MATCH']==$etag) {
    header('HTTP/1.1 304 Not Modified');
  } else {
    header('HTTP/1.1 200 Ok');
    header("Expires: " . gmdate('D, d M Y H:i:s', time()+$max_age.'GMT'));
    header("Cache-Control: max-age={$mag_age}, public, must-revalidate");
    header("Last-Modified: {$last_modified}");
    header("ETag: {$etag}");
    header('Content-type: text/css');
    echo_default_css();
    echo_custom_css();
  }
  exit;

function echo_custom_css() {
  $custom_css = get_theme_mod('my_custom_css');
  if (!empty($custom_css))
    echo "\n{$custom_css}";
}

function echo_default_css() {
  $default_css =<<<CSS
body {background-color:yellow;}
CSS;
  echo $default_css;
}

So with these three major bits of code; 1.) the add_php_powered_css() function called by the init hook, 2.) the set_my_custom_css() function called by whatever code allows the user to update their custom CSS, and lastly 3.) the my_theme_css.php you should pretty much have this licked.

Further Reading

Aside from those already linked I came across a few other articles that I thought were really useful on the subject so I figured I should link them here:

Epilogue:

But I would be remiss to leave the topic without making a closing comments.

Expires in 2020? Probably Too Extreme.

First, I don’t really think you want to set Expires to the year 2020. Any browsers or proxies that respect Expires won’t re-request even after you’ve made many CSS changes. Better to set something reasonable like 24 hours (like I did in my code) but even that will frustrate users for the day during which you make changes in the hardcoded CSS but forget to but the served version number. Moderation in all things?

This Might All Be Overkill Anyway!

As I was reading various articles to help me answer your question I came across the following from Mr. Cache Tutorial himself, Mark Nottingham:

The best way to make a script
cache-friendly (as well as perform
better) is to dump its content to a
plain file whenever it changes. The
Web server can then treat it like any
other Web page, generating and using
validators, which makes your life
easier. Remember to only write files
that have changed, so the
Last-Modified times are preserved.

While all this code I wrote it cool and was fun to write (yes, I actually admitted that), maybe it’s better just to generate a static CSS file every time the user updates their custom CSS instead, and let Apache do all the heavy lifting like it was born to do? I’m just sayin…

Hope this helps!

Leave a Comment