Caching Zend Framework Forms
Generating a form is an expensive process in ZF. It’s always bugged me that I can’t find any resources on trying to cache the initial HTML anywhere, so I took a stab at it myself. I use a loader from inside my controller action to load forms and models, so I found that was a good place to start.
Here is my initial loader class, which I have stripped down and simplified for the sake of this example. Ideally, you’d want this in something like an action helper.
<?php
class Site_Loader {
/**
* Site's controller action
* @var Site_Controller_Action
*/
protected $_controllerAction;
/**
* Sets the current controller action we're being called from
* @param Site_Controller_Action
*/
public function setControllerAction(Site_Controller_Action $action) {
$this->_controllerAction = $action;
}
/**
* Loads the requested form class
* Same conventions as Zend (Form_Name = Form/Name.php) class "Form_" . $Form_Name
*
* @param string The name of the form class
* @param string Any options for the form class constructor
* @return object Form class object - forms_$formName
*/
public function form($formName, $options = null) {
$path = APP_DIR . "/application/forms/" . str_replace('_', '/', $formName) . '.php';
require_once $path;
$class = "Form_" . $formName;
return new $class($this->_controllerAction, $options);
}
}
I always find it useful to have access to the controller in forms – especially to access the request information. Another reason is we can use it to determine the request method – more importantly, see if it’s POST. When it’s not, then we can enable caching, because the form output should not change. Only when they actually post something should we need to show anything different. I set a high cache TTL simply because I’d want it to be there forever, until I actually modify the form code, at which point I’d clear its cache. With that in mind, here is the updated (and relevant parts of) the loader.
<?php
class Site_Loader {
/**
* Site's controller action
* @var Site_Controller_Action
*/
protected $_controllerAction;
/**
* Caching object used
* @var Zend_Cache_Core
*/
protected $_cache;
/**
* Sets the current controller action we're being called from
* @param Site_Controller_Action
*/
public function setControllerAction(Site_Controller_Action $action) {
$this->_controllerAction = $action;
}
/**
* Sets the cache object to be used
* @param Zend_Cache_Core
*/
public function setCache(Zend_Cache_Core $cache) {
$this->_cache = $cache;
}
/**
* Loads the requested form class
* Same conventions as Zend (Form_Name = Form/Name.php) class "Form_" . $Form_Name
*
* @param string The name of the form class
* @param string Any options for the form class constructor
* @return object Form class object - forms_$formName
*/
public function form($formName, $options = null) {
$path = APP_DIR . "/application/forms/" . str_replace('_', '/', $formName) . '.php';
require_once $path;
$class = "Form_" . $formName;
return new $class($this->_controllerAction, $options);
}
/**
* Loads the requested form class.
* When request method is post, form is loaded normally.
* When it's not, cache is used to reduce CPU load.
*
* @param string The name of the form class
* @param string Any options for the form class constructor
*
* @return mixed Form class object, or rendered form HTML when caching is active
*/
public function formCached($formName, $options = null) {
if ($this->_controllerAction->getRequest()->isPost()) {
return $this->form($formName, $options);
}
if ($out = $this->_cache->load($id = $this->_getFormCacheId($formName, $options))) {
return $out;
}
$form = $this->form($formName, $options);
$this->_cache->save($out = $form->__toString(), $id, array(), 31536000);
return $out;
}
/**
* Generates the id used for the cache. It must be unique for the name of the
* form, and the options passed to it.
*
* @param string Form name used
* @param mixed Array of options passed to form, or null
*
* @return string Unique ID for this form cache
*/
protected function _getFormCacheId($formName, $options = null) {
if ($options === null) {
return "Form_$formName";
}
return 'Form_' . $formName . md5(serialize($options));
}
}
Enabling Caching
This is generally how a basic form action looks like in my controllers.
public function postAction() {
$form = $this->loader->form('Post_Create', $options = array());
if ($this->_request->isPost()) {
if ($form->isValid($this->_request->getPost())) {
$post = $this->model->create($form->getValues());
$this->redirector->goToUrl($post->getUrl());
}
}
$this->view->form = $form;
}
With it enabled, we’d simply change
$this->loader->form()
to
$this->loader->formCached()
Since the only interaction with my form and view script is
<?= $this->form ?>
we don’t even have to change it. The form’s __toString method will work normally when the caching isn’t active, and the form variable is just a string when it is. Simple.
Recap
This will allow for the caching of your rendered ZF forms. It is disabled automatically when the request method is POST, so it can properly re-populate your data and/or show your errors. If you use the simple form renderings in your view scripts, then they will work as-is. This is all just a proof of concept that I am testing on a few sites at the moment. If you have any feedback or questions, don’t hesitate to let me know.

What performance gains can you expect? Have you done any measurements?
It’s hard to measure because it will depend entirely on your forms and caching backend used. Zend_Form is a pretty slow library (but of course very useful), so being able to bypass it entirely for the vast majority of visitors is a huge gain, especially if you use it for things like login forms in your header, or search forms in a widget. On some of my tests, Zend_Form has taken anywhere from 0.1 to 1 or 2 seconds to generate forms of varying complexity. With caching, it would be reduced to something more like 0.001 seconds after the first initial view.
From my experience, caching entire pages can be a pain if you have lots of dynamic content, especially relative timestamps or user information on the page for the current visitor. Being able to cache specific elements has always been more useful to me.
Cheers
Pretty good in general.
The problem I see with it (without having tested it) is that if you want to protect your forms against cross-site request forgery attacks, you won’t be able to cache the forms (well, you could, but it would still be one form per user, making it useless and memory consuming).
You could use a placeholder value for a hidden form element that acts as your CSRF protection, and then just add a method to the loader or form class that performs the replacement before sending back the final HTML. With or without caching.
The whole thing is just concept code… it should be very easy to apply any necessary adjustments or tweaks that you need while still utilizing caching.
Cheers