Benjamin Tigano Developer at Large

2Feb/12Off

Early page cache plugin for Zend Framework MVC Applications

Zend Framework is one of the more popular PHP frameworks. I recommend it a lot for custom web applications, but speed is sometimes an issue. It's a very powerful framework, and as with anything, features and flexibility can effect performance where you don't need features. For simple pages (like legal statements, about, company profile, etc.), an early page cache system can help you gain performance for the less dynamic areas of a web application.

Yesterday, in an effort to test what an early page cache system would look like, I spent some time to write a plugin for a standard ZF application. I used the generic MVC application that Zend Studio will make for you, and cleaned it up a bit. Here's a breakdown of what I did to create the eary page cache plugin.

First, the ini configuration file. I added the section below to register the plugin with the front controller, and store the cache settings. Any kind of settings should ALWAYS be stored in configuration files. It can be XML, INI, or even a PHP array, but keep it separate from the rest of your application. In the case below, I'm using a lifetime of 3600 seconds (1 hour) for my cache. You'll likely want to adjust this (and the other settings) based on your application's needs.

resources.frontController.plugins.pagecache = "My_Controller_Plugin_PageCache"
pageCache.frontEnd = core
pageCache.backEnd = file
pageCache.frontEndOptions.lifetime = 3600
pageCache.frontEndOptions.automatic_serialization = true
pageCache.backEndOptions.lifetime = 3600
pageCache.backEndOptions.cache_dir = APPLICATION_PATH "/../cache/pageCache"

The next item to discuss is the Bootstrap file (/application/Bootstrap.php). While there isn't anything specific to the plugin we're creating, there is something that the plugin depends upon. In almost all of my ZF applications, I store the config in the Zend_Registry so that I can access it wherever I need it. Here is the section of code that does that:

protected function _initConfig()
{
    $config = new Zend_Config($this->getOptions(), true);
    Zend_Registry::set('config', $config);
    return $config;
}

Now, whenever we want to access the configuration settings, we can use the following code:

$config = Zend_Registry::get('config');

Another addition to the Bootstrap is registering the namespace where the plugin will live (line highlighted below):

protected function _initAutoload()
{
	// add autoloader empty namespace
	$autoLoader = Zend_Loader_AutoLoader::getInstance();
	$autoLoader->registerNamespace('My_'); // added this for the early page cache plugin
	$resourceLoader = new Zend_Loader_Autoloader_Resource(array(
	'basePath' 		=> APPLICATION_PATH,
	'namespace' 	=> '',
));

Last but not least is the plugin itself. I won't go into too much depth on this because if you're not familiar with at least the basics of a Zend MVC application, you're probably not understanding the rest of this article. We start by creating methods for the dispatchLoopStartup and dispatchLoopShutdown. We use dispatchLoopStartup because after all this is an EARLY page cache. We use the dispatchLoopShutdown because at that point, we've gone through the full dispatch loop and have a complete response that we can cache for the next request.

In the dispatchLoopStartup, we grab the configuration from the Zend_Registry that we stored it to during bootstrapping. We use those settings to create the Zend_Cache object. The first check we do is to make sure we're only caching GET requests. The next thing we do is create a cache pool key based on the page name. Next, we see if we can get a cache hit. If this is the first time we're visiting the page, it'll be a miss. If we do get a hit, we set the response based on what was in the cache, send the response, and then exit. If we didn't exit, we would go through the normal dispatch loop and avoid caching.

In the dispatchLoopShutdown, we do a few checks to make sure that the page is a good candidate for caching. If it is, we save it in the cache based on the same key we generated in the dispatchLoopStartup method.

Here is the code for the plugin class:

class My_Controller_Plugin_PageCache extends Zend_Controller_Plugin_Abstract
{
	public static $doNotCache = false;
	public $cache;
	public $key;

	public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
	{
		$pageCacheConfig = Zend_Registry::get('config')->pageCache;

		$this->cache = Zend_Cache::factory(
		$pageCacheConfig->frontEnd,
		$pageCacheConfig->backEnd,
		$pageCacheConfig->frontEndOptions->toArray(),
		$pageCacheConfig->backEndOptions->toArray());

		if (!$request->isGet()) {
			self::$doNotCache = true;
			return;
		}

		$path = $request->getPathInfo();

		$this->key = md5($path);

		$data = $this->cache->load($this->key);
		if ($data)
		{
			$response = $data;
			$this->setResponse($response);
			$this->getResponse()->sendResponse();
			exit;
		}
	}

	public function dispatchLoopShutdown()
	{
		if (self::$doNotCache
		|| $this->getResponse()->isRedirect()
		|| (null === $this->key))
		{
			return;
		}
		$this->cache->save($this->getResponse(), $this->key);
	}
}

As always, there will be cases where you don't want a particular controller or action to be cached, in that case, we've built in a static property you can set in your controller that will stop the plugin from caching the response.

My_Controller_Plugin_PageCache::$doNotCache = true;

Below is a zip file contained the full MVC application, minus the Zend Framework library (to save space.) You should drop the Zend folder inside the library folder, and create the .htaccess file (assuming you're using Apache with mod_rewrite enabled) to point to public/index.php.

If you've got questions, comments, or suggestions, leave them in the comments!

Get The Code!

Fork me on GitHub
Comments (0) Trackbacks (0)

Sorry, the comment form is closed at this time.

No trackbacks yet.