I’ve used a few different CSS/JS bundlers, but none have ever fulfilled all
that I needed. Specifically, I wanted one that could do all of the following:
- Bundle automatically with little configuration
- Optionally minify
- Optionally compress
- Allow the bundled files to be served directly by the webserver, instead of
by PHP. - Easily allow files to be served compressed, but without recompressing on every request
- Automatically detect modification the the source files, and update bundles as needed.
Thus, I created bundle-phu. Bundle-phu is a set of Zend Framework view helpers
that do all of the above.
Bundle-phu is inspired by, bundle-fu a Ruby on Rails equivalent.
Before
|
1 2 3 4 5 6 7 8 |
<script type="text/javascript" src="/js/jquery.js"></script> <script type="text/javascript" src="/js/foo.js"></script> <script type="text/javascript" src="/js/bar.js"></script> <script type="text/javascript" src="/js/baz.js"></script> <link media="screen" type="text/css" href="/css/jquery.css" /> <link media="screen" type="text/css" href="/css/foo.css" /> <link media="screen" type="text/css" href="/css/bar.css" /> <link media="screen" type="text/css" href="/css/baz.css" /> |
After
|
1 2 |
<script type="text/javascript" src="bundle_3f8ca8371a8203fcdd8a82.css?1234567890"></script> <link type="text/css" src="bundle_3f8ca8371a8203fcdd8a82.css?1234567890"></script> |
Highlights
- Changes to your source js/css is detected, and bundles regenerated automatically
- Minification can be done using either an external program (YUI compressor), or
via callback in PHP. - Compression is achieved using gzencode()
Installation
- Place the BundlePhu directory somewhere in your include_path:
|
1 2 3 4 5 |
your_project/ |-- application |-- library | `-- BundlePhu |-- public |
- Add the BundlePhu view helpers to your view’s helper path, and configure the helpers:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<?php class Bootstrap extends Zend_Application_Bootstrap_Bootstrap { protected function _initView() { $view = new Zend_View(); $view->addHelperPath( PATH_PROJECT . '/library/BundlePhu/View/Helper', 'BundlePhu_View_Helper' ); $view->getHelper('BundleScript') ->setCacheDir(PATH_PROJECT . '/data/cache/js') ->setDocRoot(PATH_PROJECT . '/public') ->setUseMinify(true) ->setMinifyCommand('java -jar yuicompressor -o :filename') ->setUseGzip(true) ->setGzipLevel(9) ->setUrlPrefix('/javascripts'); $view->getHelper('BundleLink') ->setCacheDir(PATH_PROJECT . '/data/cache/css') ->setDocRoot(PATH_PROJECT . '/public') ->setUrlPrefix('/stylesheets'); $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer'); $viewRenderer->setView($view); return $view; } } |
- Ensure your CacheDir is writable by the user your web server runs as
- Using either an Alias (apache) or location/alias (nginx) map the UrlPrefix to CacheDir.
You can also do this using a symlink from your /public directory.
e.g. /public/javascripts -> /../data/cache/js
Usage
As both these helpers extend from the existing HeadScript and HeadLink helpers in Zend Framework,
you can use them just as you do those.
|
1 2 |
<? $this->bundleScript()->offsetSetFile(00, $this->baseUrl('/js/jquery.js')) ?> <? $this->bundleScript()->appendFile($this->baseUrl('/js/foo.js')) ?> |

How quick is it, or are you using something like fork or queuing?
With minification disabled, it’s _very_ quick. I profiled it with my application that was using the regular Zend_View_Helper_HeadXxxx classes, and the ZF toString() method took 1ms on average. Using BundlePhu in the best case, where the file is already cached took 500us on average. If the bundle is invalidated and is regenerated, the toString() time averages about 5ms. When using compression it averages 18ms.
Minification is a huge time sink though depending on how you use it. When using YUI compressor (which isn’t really intended for on the fly use) the worst case time was an additional 2-3s on the page load. If you want the additional bandwidth savings of minification you have to weigh whether that initial cost of generating the bundle is ok in your app.
Personally, right now I’m using it with minification disabled, until I can possibly find a faster solution.
Thank you for sharing. Some notes:
1. setDocRoot() path should be with trailing slash (PATH_PROJECT . ‘/public/’) in your examples;
2. There are also jQuery (and Dojo I guess) specific helpers to insert JavaScript/CSS code. Are you going to support them?
3. Now it is required to replace all headScript() calls with bundleScript(), but it’s much better do not modify any existing sources and use helper like this:
bundleScript($this->headScript(), $this->jQuery()); ?>
As you see it is also the possible way to support jQuery/Dojo. Are you interested in this or I should patch your helpers myself?
Thanks for the comments Vladimir:1. No. Why?2. I don’t use either of those and thus have no plan to.3. I’m aware of this limitation. I initially wrote them as drop in replacements for the HeadLink & HeadScript helpers, but changed my mind. There are several good reasons:
* These are not 100% compatible with the existing ZF helpers. Things like setting the charset, or setting the “type” to something besides “application/javascript” or “text/css” are not supported and would behave unexpectedly.
* Capturing dynamically generated content: var foo = ; would result in the initial value being cached forever.
* There would be no easy way to use the standard ZF helpers if so desired.
* It’s trivially easy to do a s/headScript/bundleScript/ on your entire project.
>>1. No. Why?
$mtime = filemtime($this->_docRoot . $src);
I am using relative $src in script tag () so at least it is required for me.
I could only get this to work if I appended my scripts in a layout file. What if I have multiple layout files? Can you please give an example of how to append scripts in the Bootstrap file? I have append commands like this one after I getHelper()
$view->bundleScript()->appendFile(‘/_ui/scripts/lib/jquery-1.4.2.min.js’);
But nothing outputs. Could it be that my paths are wrong but there is no error?
Hi Marcy,
Could you please use the issue tracker for this on github?
https://github.com/hobodave/bundle-phu/issues
Thanks.
Actually it did end up working…. But I’m not sure why. I think it might have been my file paths. Cheers!
->setMinifyCommand(‘java -jar yuicompressor -o :filename’)
How I can use only php without jar? For example http://code.google.com/p/minify/ ?
We are having permissions issues with the bundle script files when we push to different servers. Is there a way to configure the permissions of the bundle files when they are created?
This should be done using standard UNIX permissions. See umask: http://php.net/umask
Sweet, thanks Dave! I’ll definitely check it out.
You’ll also probably want to use the setgid bit on the directory (chmod g+s). This permits files created within it to inherit the group owner of the directory. See http://en.wikipedia.org/wiki/Setuid
“Using either an Alias (apache) or location/alias (nginx) map the UrlPrefix to CacheDir. You can also do this using a symlink from your /public directory. e.g. /public/javascripts -> /../data/cache/js”
I donot understand this section of installation. and may be for this reason I couldn’t make it work. please help me
UrlPrefix is how your JS files will be prefixed in the request URI. For example, an UrlPrefix of /javascripts will result in URIs of /javascripts/bundle12345.js.
CacheDir is the location on disk of where the bundles are written.
These are separate because it is a very good practice to not have your /public directory writable. For Zend Framework if you are following the recommended project structure then the proper location for written files is /application/data (relative to project root).
This means you need to do one of two things to map the /javascripts URI to the /application/data/js disk location.
1. You can use a symlink (often the easiest)
2. You could use the appropriate mechanism of either Apache, or Nginx to map the request URI to the proper disk location. You would need to consult the documentation for this.
I’m getting “stat failed” at line 343 BundleScript for filemtime function. any help
This simply means that the attempt to write the $cacheFile (in either _writeUncompressed or _writeCompressed) is failing and the file doesn’t exist. This would be a permissions issue that you need to diagnose and fix on your system.