Combine and Compress CSS Files with PHP

A few years ago I was frustrated by the lack of an exceedingly fast and simple1 open-source photo portfolios for my London-based photography website, so I wrote and open sourced Turbo Photo Portfolio (TPP) on github. This post is about one small component, a PHP script that combines and compresses2 multiple CSS files.

Before HTTP/2, performance related best practice dictated reducing the number of distinct resources3; thus, I created a PHP script which would combine multiple CSS files into one resource request, as well as apply some basic minification.

The latest version of my CSS loader can be found on github. At some point I plan to test a simple cache mechanism to see if that makes a worthwhile performance difference.

The usage is very simple, first edit the array at the top of the file with the CSS filenames and then add the following to the <head> of your HTML:

<link href='css/css_loader.php' rel='stylesheet' type='text/css'>

I’ve also reproduced the current (Jan 2016) version here to make copy-and-paste even easier.

<?php
/**
 * Combines and minifies specified CSS files. Tested with https://redbot.org
 *
 * NB. Order of output not guaranteed, e.g. minified css files are output first.
 * Also, services like Cloudflare seem to break the 304 functionality.
 */
// The CSS files to combine
$cssFiles = array('milligram.min.css', 'tpp.css', 'lightbox.min.css');

/* HTTP headers */

// Calculates last modified and md5 of CSS files and this php script
$lastModified = filemtime(__FILE__);
$fileHash = md5_file(__FILE__);
foreach ($cssFiles as $file) {
    $fileHash .= md5_file($file);
    $mod = filemtime($file);
    if ($mod > $lastModified) {
        $lastModified = $mod;
    }
}
$eTagHash = md5($fileHash);

// Sends headers back to the client
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
header('ETag: "' . $eTagHash . '"');
$secondsPerWeek = 60 * 60 * 24 * 7;
// set cache age to 1 week
// omitting 'must-revalidate' allows serving of stale cache
// further reduces network traffic i.e. no need to check for 304
header('Cache-Control: max-age=' . $secondsPerWeek);

// check if page has changed. If not, send 304 and exit
if ((isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
        &&
        strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $lastModified)
    ||
    (isset($_SERVER['HTTP_IF_NONE_MATCH'])
        &&
        strpos($_SERVER['HTTP_IF_NONE_MATCH'], $eTagHash) !== false))
{
    header('HTTP/1.1 304 Not Modified');
    exit();
}
// Content-type needed for the body, not required if we send the 304 above.
header('Content-type: text/css');

/* Send the content */

// output minified CSS input files first i.e. file names ending in .min.css
foreach($cssFiles as $key => $file) {
    if (substr($file, -8) === '.min.css') {
        include($file); // output the already minified file
        unset($cssFiles[$key]); // remove file name from file list
    }
}

function simpleCSSMinify($buffer) {
    // remove comments, new lines and tabs
    $regexToRemoveArray = array('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '/[\n\t]+/');
    $buffer = preg_replace($regexToRemoveArray, '', $buffer);
    // remove multiple spaces
    $buffer = preg_replace('/\s+/', ' ', $buffer);
    // remove spaces after , and ;
    $buffer = str_replace(', ', ',', $buffer);
    $buffer = str_replace('; ', ';', $buffer);
    return $buffer;
}

// output css files after running them through the minify function first
ob_start('simpleCSSMinify');
foreach($cssFiles as $file) {
    include($file);
}
ob_end_flush();
?>

  1. by simple I mean few lines of code and few dependencies ↩︎

  2. basic minification ↩︎

  3. I think is still current best practice until we see HTTP/2 all but replace HTTP/1.1 ↩︎