CSS/JS/Asset Compression in CakePHP

This article is over a year old and may contain outdated information.
This code was developed for CakePHP 1.2 and will no longer work on the most recent versions. You should be using an asset handler plugin in 1.3+.

The other day I spent way too many hours trying to figure out CakePHPs built in CSS and JS compression system. To say the least, its not really built in, you have to configure and do a lot of it yourself. I did much searching and found tutorials that didn't work, or using custom built helpers that were of no use, and in the end I wrote my own script (based off webroot/css.php written by gwoo). My script does the following:

  • Compresses both CSS and JS, using one file
  • Caches the assets, and recaches if a change is made to the original
  • Comes prebuilt with CSS compression
  • Must download and install JSMin
  • Extremely easy to install and configure

To use my asset compression script, create a file called assets.php within webroot, and then copy and paste the code found at the end of this post. Once you have done this, download and place the jsmin.php file within /vendors/ or /app/vendors/ (the JSMin file should be named jsmin.php to work). The next step is to create a folder called assets within your /app/tmp/cache/ directory, and chmod the permissions to 777. The final step is to uncomment the filtering in core.php and set the path to assets.php.

Configure::write('Asset.filter.css', 'assets.php');
Configure::write('Asset.filter.js', 'assets.php');

Now to take it for a test spin! Once you have uncommented the configuration, direct your browser to the actual JS and CSS paths to see if it works (http://www.domain.com/ccss/style.css), and that's it! Hope this has been helpful and easy, like it should have been!

<?php 
// No cake installation
if (!defined('CAKE_CORE_INCLUDE_PATH')) { 
	header('HTTP/1.1 404 Not Found');
	exit('File Not Found'); 
} 

// Get asset type
$ext = trim(strrchr($url, '.'), '.');
$assetType = ($ext === 'css') ? 'css' : 'js';

// Wrong file
if (preg_match('|\.\.|', $url) || !preg_match('|^c'. $assetType .'/(.+)$|i', $url, $regs)) {
	die('Wrong File Name');
}

$cachePath = CACHE .'assets'. DS . str_replace(array('/','\\'), '-', $regs[1]);
$fileName = $assetType .'/'. $regs[1];

if ($assetType == 'css') {
	$filePath = CSS . $regs[1];
	$fileType = 'text/css';
} else {
	$filePath = JS . $regs[1];
	$fileType = 'text/javascript';
}

if (!file_exists($filePath)) {
	die('Asset Not Found');
}

/**
 * Compress the asset
 * @param string $path
 * @param string $name
 * @return string
 */
public function compress($path, $name, $type) {
	$input = file_get_contents($path);
	
	if ($type == 'css') {
		$stylesheet = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $input);
		$stylesheet = str_replace(array("\r\n", "\r", "\n", "\t", '/\s\s+/', '  ', '   '), '', $stylesheet);
		$stylesheet = str_replace(array(' {', '{ '), '{', $stylesheet);
		$stylesheet = str_replace(array(' }', '} '), '}', $stylesheet);
		$output = $stylesheet;
	} else {
		App::import('Vendor', 'jsmin');
		$output = JSMin::minify($input);
	}
	
	$ratio = 100 - (round(strlen($output) / strlen($input), 3) * 100);
	$output = "/* File: $name, Ratio: $ratio% */\n". $output;
	return $output;
}	

/**
 * Cache the asset
 * @param string $path
 * @param string $content
 * @return string
 */
public function cacheAsset($path, $content) {
	if (!is_dir(dirname($path))) {
		mkdir(dirname($path));
		chmod($path, 0777);
	}
	
	if (!class_exists('File')) {
		uses('file');
	}
	
	$cached = new File($path);
	return $cached->write($content);
}

// Do compression and cacheing
if (file_exists($cachePath)) {
	$templateModified = filemtime($filePath);
	$cachedModified = filemtime($cachePath);

	if ($templateModified > $cachedModified) {
		$output = compress($filePath, $fileName, $assetType);
		cacheAsset($cachePath, $output);
	} else {
		$output = file_get_contents($cachePath);
	}
} else {
	$output = compress($filePath, $fileName, $assetType);
	cacheAsset($cachePath, $output);
	$templateModified = time();
}

header("Date: ". date("D, j M Y G:i:s", $templateModified) ." GMT");
header("Content-Type: ". $fileType);
header("Expires: ". gmdate("D, j M Y H:i:s", time() + DAY) ." GMT");
header("Cache-Control: max-age=86400, must-revalidate"); // HTTP/1.1
header("Pragma: cache_asset");        // HTTP/1.0
echo $output; ?>

34 Comments

  • not work in cakephp 2.x
  • While trying this for CSS cache it gives me the error as -
    http://mydomain/ccss/styles.css was not loaded because its MIME type, "text/html" is not "text/css".

    Can anyone please suggest what is the issue here.

    Thanks
  • There is this plugin https://github.com/glaucocustodio/easy-compressor-plugin for CSS and Js compression too.
    Anonimous
  • For who need this script, i have inserted a patch that work with plugins.

    On row 27 delete ^ char in preg_match

    On row 45 add

    if (isset($this->params['plugin'])) {
    $filePath = APP . 'plugins' . DS . $this->params['plugin'] . DS . WEBROOT_DIR . DS . $assetType . DS . $regs[1];
    }
  • For how as need of this script, i have applied a patch that use
    assets of plugins.
  • Thank you for your script,

    It works well with my site, but there is a problem occur when I use theme.

    Does this source code work with theme? Is there any idea?

    Thanks
  • for those how's still using this and have problem with broken js plugins like tinymce and sliders just create 2 folders ccss / cjs and put a copy of the plugin's js and css files in them

    it'll work like a charm

    and don't forget Ron Chaplin 's :

    if(Configure::read('debug') == 0)
    {
    Configure::write('Asset.filter.css', 'assets.php');
    Configure::write('Asset.filter.js', 'assets.php');
    }

    easiest way there's
  • thank you !

    need just compression, minify doesn't work as well as this if you have dozen of css(framework?) and javascript files.
  • Great post! Thank you!! How do you clear this cache files?
  • Thank you! The instructions are easy to follow and everything works perfectly.
  • it doesn't let me post the code here but here is the code link
    http://bin.cakephp.org/view/1250547562
  • here is workground for plugins css and js
  • it doesn't handle plugin js's and css's any fix for this ???
  • Hy, thanks for that code it's very helpfull.

    Just a question, when creating the compress file in the cache, the date of creation is at +1 hour of the server time.

    was looking why but didn't found.

    If you have any idea.

    Best regards
    Woodruff
  • Thanks for your script.
  • This is just what we're looking for. I'm surprised this hasn't made it to the CahePHP core yet. Thanks!
  • Love the code and much easier to set up than cake's core...

    Noticed one mistake. You have double and triple white spaces replaced with none, which caused me an error when I had .pagination div (2 spaces) being collapsed into none. A solution would be to use a preg replace to trim all excess white space:

    $stylesheet = preg_replace('/\s\s+/', ' ', $stylesheet);
  • Thank you - I had trouble getting another one to work, so I'm glad this one does.
  • One thing that I have noticed is that when using the debugkit, which is a plugin, as stated by others this breaks the relative location of assets.

    A workaround I employed was to wrap the Configure section in a if block.
    
    if(Configure::read('debug') == 0)
    {
        Configure::write('Asset.filter.css', 'assets.php');
        Configure::write('Asset.filter.js', 'assets.php');
    }
    


    As I have stated, this is simply a workaround, and applies logically, due to debugkit not being used unless debug lvl is set to 2, and simply the fact that if your in debugging phase, it's probably useful to be able to read your css/js in their natural state.
  • this is the best post about this subject, the guys who should read it should have basic http headers background...
    Robust Solution
  • To made TinyMCE work one solution is put the subdirectories of tinymce on /cjs like is on /js.

    example: /app/weebroot/js/tinymce/tinymce_subfolders.

    just copy to /app/weebroot/cjs/tinymce/tinymce_subfolders.

    Works for me.

    Same works with Shadowbox.
    krusty999
  • Hey Miles,
    thanks for the nice script!

    I just noticed that relative paths to images (background, etc) get broken! This is also the problem with tinymce.

    Cheers again!

    Dan
  • Hi, good post. I have been woondering about this issue,so thanks for posting. I'll definitely be coming back to your site.
  • Yeah, it breaks TinyMCE
  • Hello, how about make this mergue all css before compress them.
    Thanks!
    krusty999
  • Thanks a lot, it's really easy to install and it works very well.

    But it breaks tinymce
    gargamel
  • Thanks a lot, it's really easy to install and it works very well.

    But it breaks tinymce
    gargamel
  • Wow, how incredibly easy! This is how it should be built right into cake!
  • Nice work, do you mind if I put this in my cakephp-snippets repository at Github? (of course, your name will be on it, not mine. It's just for reference.)

    Please let me know, you have my e-mailaddress :).
  • @topherez - No it just compresses the css/js and creates a cache file for it.
  • Does this take care of gZipping components as well?
    topherez
  • I think I'd like this to do two more things on top of this code. Do you have this one github for forking?
  • Thanks for sharing the code, I'll be giving this method a go.
  • This seems to be much lighter and "cakier" than Php Speedy that I'm using right now. I'll give it a try Thanks!