Caching files statically with Zend Framework
I've been using ZF (almost exclusively) since version 0.10 or so in 2006. It's come a long way since then, and the folks involved with it are very skilled and methodical. It's quite fun to see new versions roll out and see the various proposed components on the wiki come to life over time.
I do find the documentation to be a bit lacking at times, however. For instance, I was messing around with the Zend_Cache_Manager last night, and discovered templates for the "page" and "pagetag" caches, which led me to the Cache action helper. It seems that this component is completely undocumented. I found the proposal on the wiki, though, and after I read through the code I played around with it for a new project. And I have to say, it's rather neat.
So, it provides a caching mechanism using Zend_Cache_Backend_Static, which is a cache that will write out static files that can be served by the web server directly, without invoking PHP at all. And the cache action helper lets you invalidate the generated pages easily as well. Let's say you have a forum, and you're caching each thread statically, then when someone adds a reply, you'd bust the cache.
First, you'll want to tell the cache manager about where to store the cached pages. It defaults to "public/" but that's where I put hand-coded pages, and I don't want to just throw automatically generated pages in there as well. So I added the following to my application.ini:
resources.cacheManager.page.backend.options.public_dir = APPLICATION_PATH "/../public/_cached"
And then I created that directory. And made it world-writable.
Next, the logic added to the ThreadController, where I want to control what's getting cached:
<?php
/**
* Forum thread controller
*
* Handles viewing threads
*/
class ThreadController extends Zend_Controller_Action
{
public function init()
{
// the view action is cachable.
$this->_helper->cache(array('view'));
}
/**
* View a forum thread
*
* URL: forums.example.com/thread/<id>
*/
public function viewAction()
{
// get thread id from URL
$threadId = $this->_getParam('id', 0);
// Pull forum posts for this thread from the service tier
$service = new App_Service_Forums();
$posts = $service->findPostsByThreadId($threadId);
// Feed the view
$this->view->posts = $polls;
}
/**
* Reply to a forum thread
*
* URL: forums.example.com/thread/reply/<id>
*/
public function replyAction()
{
// get thread id from URL
$threadId = $this->_getParam('id', 0);
// we want the request
$request = $this->getRequest();
// The form that users will compose the forum reply in
$form = new App_Form_Forum_Reply();
if ($request->isPost() and $form->isValid($request->getPost())) {
// Form was submitted, so process it
// Post a reply to the thread via the service tier
$service = new App_Service_Forums();
$reply = $service->replyToThread($threadId, $form->getValues());
// also clear the cache for the URL
$this->_helper->cache->removePage('/thread/' . $threadId, true);
$this->_redirect('/thread/' . $threadId);
}
$this->view->form = $form;
}
}
Then, there are also a few pitfalls. For one, you have to turn off the front controller's output buffering, otherwise you end up with empty cache files. If you're using an .ini file to drive application configuration, you'll want to add
resources.frontController.params.disableOutputBuffering = true
And second, you need to tweak your web server to try to serve those cached files first. So my .htaccess file looks like this
RewriteEngine On
# Serve cached pages if they exist
RewriteRule ^/(.*)/$ /$1 [QSA]
RewriteRule ^$ _cached/index.html [QSA]
RewriteRule ^([^.]+)/$ _cached/$1.html [QSA]
RewriteRule ^([^.]+)$ _cached/$1.html [QSA]
# Hit files, symlinks and directories directly, if they exist.
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
# Everything else hits the application.php
RewriteRule ^.*$ /application.php [NC,L]
And I think I just discovered a bug in the Zend_Cache_Backend_Static::removeRecursively() method, which doesn't remove the directory properly.
Still trying to find a way to scale this beyond a single server, since there's no distributed cache backend that supports tagging. Would have to use file based caching with an NFS share or something, and that doesn't seem all that optimal.
Print This Post