Automatic JavaScript and CSS Packer Helper for CakePHP

Posted by Matt on Wed, Aug 08 2007

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.

Posted in Sandbox

23 Comments

Adz said on Aug 21, 2007
This is a nice little helper you've made! I also had a look at Brad's implementation and wondered why he did it like that. A console script would make more sense for him since he releases a product which he has no control over once a user installs it on their server - therefore eliminating the need to make it dynamic.
R. Rajesh Jeba Anbiah said on Nov 30, 2007
When you add lot of files, it gets crazy in Windows as it's generating a very long cache filename. I suppose md5() would solve it.
Matt said on Dec 07, 2007
True the cache names can get long. I debated using an md5 to shorten them, but I liked maintaining some meaning in the filename. If it's causing a problem for you, then you should definately implement it.
Matt said on Dec 07, 2007
Hey everyone,
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')) {"
seme said on Dec 09, 2007
In the header section of the layout, I have the following:
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)
seme said on Dec 09, 2007
echo googlelink is basically the following:

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 !!
Matt said on Dec 10, 2007
Hey Seme,
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
seme said on Dec 10, 2007
Hey 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.
seme said on Dec 10, 2007
< ?php and echo statements were removed from my last post !! here is my original post:

http://bin.cakephp.org/view/671080668
Matt said on Dec 10, 2007
Seme,
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.
Marc said on Dec 26, 2007
I just created Asset Mapper using your Asset Packer as a base to build on top of.

http://marcgrabanski.com/code/asset-mapper/
R. Rajesh Jeba Anbiah said on Jan 18, 2008
Glad to find md5() support (I already added it locally)

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
Matt said on Jan 18, 2008
Hey Rajesh,
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
R. Rajesh Jeba Anbiah said on Jan 19, 2008
Minifier is the right choice and at least for me Dean packer's packed scripts are freezing browsers.

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.
Matt said on Jan 20, 2008
Hey Rajesh,
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.
Nathan said on Apr 24, 2008
Matt,

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);
}
Jaime Gómez Obregón said on Nov 01, 2008
Thank you very much for this great helper!

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.
Jaime Gómez Obregón said on Nov 01, 2008
Yet another suggestion: on the .htaccess file the "application/javascript" mime type is not set for the gzip compression nor the expiration date.
Matt said on Nov 01, 2008
Hey 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.
Wilson said on Nov 21, 2008
Hi Matt,

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
Matt said on Nov 21, 2008
Hey 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
Wilson said on Nov 22, 2008
Interesting. I have the latest Cake, etc. But it worked when I changed line 169 to this:

App::import('Vendor', 'csstidy', array('file' => 'csstidy/class.csstidy.php'));

I have the same setup as you:

/vendors/csstidy/class.csstidy.php
Matt said on Nov 22, 2008
Hey Wilson,
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