Automatic JavaScript and CSS Packer Helper for CakePHP
Download
http://github.com/mcurry/cakephp/tree/master/helpers/asset
The Story
A little while ago I wrote a post about how I manually combine/pack Javascript files. Even as I was writing it I was thinking "hey, this would be a great CakePHP plugin/component/helper. Before I could code it Brad Daily beat me to it. However there are a couple of things I didn't like about his method, particularly the need for configuration files (how anti Cake!) and the need to run a console script to generated the packed versions.
My First Solution
Suitably inspired I whipped up my own version. And it was awesome. It was so friggen automagic that you didn't even have to change your code. Just include the helper and it did the rest - assuming you were using the new (as in Cake 1.2) scripts_for_layout syntax and not adding your scripts inline.
The Snag
Unfortunately I had missed a small issue. Since the helper relied on the afterRender callback to do most of the work any js/css added in the layout was missed. See the afterRender is called after the view is rendered, but BEFORE the layout is rendered. Doh! Digging through the base helper I saw a promising callback, afterLayout. After a bit more digging I found that this is merely a placeholder (added sometime around new years) and isn't actually called anywhere. I'm not sure if it would have worked in my case anyway, since I really needed something like duringLayoutHeaderRender callback.
My Second Solution
So I reworked the interface of the helper. It's less automagic now, but works with scripts from both the layout and view. Basically instead of just echoing $scripts_for_layout you use the helper. Otherwise you still include JavaScript and CSS as normal, making sure to set inline to false. Full directions and an example are in the Sandbox as usual.
Pros and Cons
I like Brad's helper better for certain projects. Particularly ones that uses a large amount of JavaScript, as his photo manager app does. But I think for most small sites, with just a few files the automatic approach works out better. Plus mine packs CSS files in addition to JS. This is probably a good time to mention that Brad's app, SlideShowPro, is totally awesome. I recently used it when developing AndrewSaul.com. Basically I went from no photo gallery to full featured, cool looking, seamlessly integrated photo gallery in thirty minutes. Seriously, I was shocked when I read this was the product of just two guys working nights.
Bringing It Home
Checkout the helper. It's kinda cool. The Sandbox jumped from a 64 to a 79 in YSlow after I applied it. I also included an .htaccess file that handles things like gzip, expire time and ETAGS. Unfortunately my host doesn't support any of those, so I didn't get any benefit from it.

23 Comments
The new Cake 1.2 release changes the way configuration is handled. So on line 51 or so change "if(DEBUG) {" to "if (Configure::read('debug')) {"
http://bin.cakephp.org/view/1193049833
Unfortunately, the JS files are not merged, nor are they minified.
Any hints why ? (using a nightly build of Cake1.2 5818 2007-10-20 12:01:30Z)
link("http://maps.google.com/maps?file=api&v=2.87&key=ABQIAAAAekfK_HXex-q1Cusfgf6odhT2PGOO4vxN2giRORTFL_PjFK2ZqhQKfE6tErd4H2dAP_MHUb8lz0szUA"); ?>
still can't figure out why it is not working !!
A couple things to check.
1) Did you make the change I detailed in comment 4?
2) Is debug set to 0?
3) Try seperating the adding of the javascripts into individual lines instead of using the array() approach.
-Matt
Thanks for the fast response.
Updating line 51 in the Asset.php was one of the first things I've done. Separating the addition of files did not make any difference.
However, changing debug from 2 or 1 to 0 yielded a blank page with only the page title displayed (body part is empty)!!!
I thought the problem might have to do with Google Maps, so I tried your method with a different page that doesn't call or make use of the Google Maps API,
I placed the following lines in the Header section of the layout:
link('jquery/jquery-1.1.4.pack.js',false)); ?>
link('jquery/jquery.lavalamp.js',false)); ?>
link('jquery/jqModal.js',false)); ?>
codeBlock(' var $j = jQuery.noConflict();'); ?>
link(array('forms.js',false)));?>
link('prototype/prototype.js',false));?>
link('search.js',false)); ?>
scripts_for_layout(); ?>
and unfortunately, no JS files were included in the header, and the page still is blank (debug is still set to 0)
And then, I tried the following:
link('jquery/jquery-1.1.4.pack.js',false)); ?>
link('jquery/jquery.lavalamp.js',false)); ?>
link('jquery/jqModal.js',false)); ?>
codeBlock(' var $j = jQuery.noConflict();'); ?>
link(array('forms.js',false)));?>
link('prototype/prototype.js',false));?>
link('search.js',false)); ?>
scripts_for_layout(); ?>
and only var $j = jQuery.noConflict();'); was outputted in the header !!
When changing the code to what I originally had (before trying to make use of Asset.php):
link('jquery/jquery-1.1.4.pack.js')); ?>
link('jquery/jquery.lavalamp.js')); ?>
link(array('jquery/jqModal.js'))); ?>
codeBlock(' var $j = jQuery.noConflict();'); ?>
link(('forms.js')));?>
link('prototype/prototype.js'));?>
link('search.js')); ?>
scripts_for_layout(); ?>
The page worked perfectly fine, and everything was displayed correctly. This is how I left it for now. The JS files are not merged, nor minified, and I didn't manage to get the helper to work.
http://bin.cakephp.org/view/671080668
The helper only does its thing when debug is 0, which can make it a bit tricky to debug. Try setting debug back to 2 and commenting out the return line on 52.
It sounds like you're generating a PHP error, but not seeing the error with debug set at 0. I bet its a problem with one of the vendor files.
http://marcgrabanski.com/code/asset-mapper/
Some notes:
1. The JavaScript packer is not a packer but minifier
2. $tidy->settings['merge_selectors'] = false; needed to avoid messing up of CSS styles/rules
3. Doesn't seem to handle codeblock
4. Some JavaScript such as sIFR needs separate script block, in that case this helper doesn't seem to help. I'm thinking of adding prefix based configurations (e.g., x-foo.js (will exclude minify), s-foo.js (will minify and place in separate script block), p-foo.js (will pack instead of minify), etc)
5. I think, the invocation has to be placed in element and also has to be cached 'coz it seems to call asset helper always
Thanks for the comments. You're right about the minifier/packer terms. When I was writing this I was trying to decide between JSmin and the PHP version of Dean Edwards Javascript Packer. I treated the words as interchangeable, but they clearly have different meanings.
Good tip on the merge_selectors. It's probably not a great idea to use a feature that is described in their docs as "Very basic and has at least one bug. Hopefully there is a replacement soon."
As for the codeblock thing...uh...I kind of figured this was a problem and be honest I don't think I've ever used codeblock.
Can you explain #5 more - I'm not sure I follow. The helper is called every time to checks if it need to re-generate the files, but it doesn't actually do the processing unless it needs to.
-Matt
Marc seems to have added codeBlock support; but I personally feel that his helper is overkill.
The major reason for choosing the Asset Helper is to speed up the site. But, the hit on the helper call is heavy--at least for me. Yes, I seems to be wrong on that using element with caching can help.
Over the top of my head, though Asset Helper is godsend, for the real benefit, one seems need to manually minify/pack.
Marc emailed when released his version. I keep meaning to check it out, but haven't had the time. I did find the time to check out the performance hit of the helper...and write a post about it. I'm interested to know if the results I found are significantly different then what you're seeing.
Thank you so much for this. It helped a bunch. One thing, the version I got didn't work unless I had it in debug mode 0. I needed to change a few lines of code to get it going. It may be my version of CakePHP but who knows. Anyways, here is the code..
$folder =& new Folder();
//make sure the cache folder exists
if (!file_exists($path . $this->cachePath) && $folder->create($path . $this->cachePath, "777")) {
trigger_error('Could not create ' . $path . $this->cachePath
. '. Please create it manually with 777 permissions', E_USER_WARNING);
}
That should replace this,
$folder = new Folder;
//make sure the cache folder exists
if ($folder->create($path . $this->cachePath, "777")) {
trigger_error('Could not create ' . $path . $this->cachePath
. '. Please create it manually with 777 permissions', E_USER_WARNING);
}
I just want to report a little issue. After implementing it on a site I found corrupted CSS. Having a look closer I found that cssTidy was incorrectly compressing decimals - i.e "font-size: 1,5em" instead of "1.5em".
That was because cssTidy is using locale-sensitive functions, and at the top of bootstrap.php file I manually force my locale with "setlocale(LC_ALL, 'es_ES.utf8')". That's necessary, because after all I want my app to be correctly localized.
I solved it by adding this:
setlocale(LC_ALL, 'POSIX');
At the very top of your AssetHelper::process() method.
HTH,
Jaime.
Thanks for the info about setting locale. I'm sure this will be helpful to other users.
I also added "application/javascript" to the sample htaccess file.
Thanks for such a useful script!
I've got the lastest Cake, JSMin and cssTidy on my dev system. But my Asset Packer is stopping at the CSS. I can run a debug and find that Asset.php is OK up to line 169 then dies silently. There's nothing in the logs.
Just curious if anyone's experienced this. I tried extra PHP memory, etc. And various permissions.
Thanks for any advice.
Best,
Wilson
Line 169 is the App::import for csstidy, right?
I would guess either the path is different in your vendors dir or it's a permissions thing.
I have mine as /vendors/csstidy/class.csstidy.php. I also have all the other csstidy files in that directory as well. Not sure if it depends on any of them.
-Matt
App::import('Vendor', 'csstidy', array('file' => 'csstidy/class.csstidy.php'));
I have the same setup as you:
/vendors/csstidy/class.csstidy.php
Glad you got it working. I have mine in the root vendors dir. Is yours in the /app/vendors? Maybe that's the difference?
Add new comment