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.