As a Zend Framework user I use a lot the Zend_Form component to render my forms. I like it, it’s easy to use, pretty versatile and comes with lots of ready built filters and validators meant to make life easier. But the problems with Zend_Form begin when one tries to style it. Problem partially solved by the developers of ZF with the use of the decorator pattern.

Using decorators, the form’s elements can be wrapped in custom HTML tags and alter the way the form displays.

But in some cases, for example when you want each element of the form to have its custom markup, to have 3 elements on the first line and 2 on the second or you want to have all the errors grouped in a single area like in Drupal, the decorator pattern fails. Bad. What to do then? Stop using Zend_Form? Or hacking it? If the second choice sounds better, then keep reading :P

How to style a Zend_Form

I did it this way: overrode the __toString() method to return HTML code from a view, passed the form object to this view and rendered it there – by hand, without any decorators. Sounds simple? It is! First of all, I’ve wrote a custom form class (pay attention to the __toString() method):

/**
 * Example form
 *
 * @package Forms
 * @author Tudor Barbu <miau@motane.lu>
 */
class MyForm extends Zend_Form {
    /**
     * init the form
     *
     * @return void
     */
    public function init() {
        $this->setMethod('post');
        $translator = Zend_Registry::get('translator');

        $email = new Zend_Form_Element_Text('email');
        $email->setOptions(
            array(
                'label'          => $translator->translate('Email address'),
                'required'     => true,
                'filters'        => array('StringTrim','StripTags',),
            )
        );    

        $name = new Zend_Form_Element_Text('name');
        $name->setOptions(
            array(
                'label'          => $translator->translate('Name'),
                'required'     => true,
                'filters'        => array('StringTrim','StripTags',),
            )
        );    

        $message = new Zend_Form_Element_Textarea('message');
        $message->setOptions(
            array(
                'label'          => $translator->translate('Message'),
                'required'     => true,
                'filters'        => array('StringTrim','StripTags',),
            )
        );

        $submit = new Zend_Form_Element_Submit('submit');
        $submit->setOptions(
            array(
                'label' => $translator->translate('Submit'),
            )
        );

        $this->addElement($email);
        $this->addElement($name);
        $this->addElement($message);
        $this->addElement($submit);
    }

    /**
     * magic method __toString() implementation
     *
     * @return string
     */
    public function __toString() {
        // get the view
        $view = Zend_Layout::getMvcInstance()->getView();
        // use addScriptPath to tell the view where to look for this files
        // previous version used setScriptPath here, a bad practice
        // read  Iulian Ilea's comment for more details
        $view->addScriptPath( APPLICATION_PATH . '/views/forms/' );

        // pass the form the the view
        $view->form = $this;

        // render the form using myForm.phtml
        return $view->render( 'myForm.phtml' );
    }
}

Now, in the application/views folder, I’ve created a subfolder called forms, which holds this kind of views, meant to render forms. I usually name these files after the form they belong to, so if the form was saved in a file called MyForm.php, this view will be called myForm.phtml.

Pretty self explanatory. The content of myForm.phtml looks like this:

/**
 * custom form renderer
 *
 * @package Forms
 * @author Tudor Barbu <miau@motane.lu>
 */

// get the form's elements as variables
foreach($this->form->getElements() as $element) {
    ${$element->getName()} = $this->splitElement($element);
}
?>
<table>
    <tr>
        <td>
            <?php echo $email['label'];?>
        </td>
        <td>
            <?php echo $name['label'];?>
        </td>
    </tr>
    <tr>
        <td>
            <?php echo $email['body'];?>
            <?php if(!empty($email['messages'])):?>
                <ul>
                    <?php foreach($email['messages'] as $message):?>
                    <li><?php echo $message;?>
                    <?php endforeach;?>
                </ul>
            <?php endif;?>
        </td>
        <td>
            <?php echo $name['body'];?>
            <?php if(!empty($body['messages'])):?>
                <ul>
                    <?php foreach($body['messages'] as $message):?>
                    <li><?php echo $message;?>
                    <?php endforeach;?>
                </ul>
            <?php endif;?>
        </td>
    </tr>
    <tr>
        <td colspan="2">
            <div style="display:none">
                <!-- only for screen readers -->
                <?php echo $message['label'];?>
            </div>
            <?php echo $message['body'];?>
        </td>
    </tr>
    <?php if(!empty($message['messages'])):?>
    <tr>
        <td colspan="2">
            <ul>
                <?php foreach($message['messages'] as $msg):?>
                <li><?php echo $msg;?></li>
                <?php endforeach;?>
            </ul>
        </td>
    </tr>
    <?php endif;?>
    <tr>
        <td colspan="2">
            <!-- use a button tag instead of a input type submit -->
            <button type="submit">
                <?php echo $submit['label'];?>
            </button>
        </td>
    </tr>
</table>

I’ve wrote the SplitElement view helper to make things easier. It’s supposed to be pretty simple, but if you have a hard time understanding it, post a comment below. Here is its source code.

/**
 * Returns an array containing the element's
 * body (html tag), label text and error messages
 *
 * Expect something like:
 *
 * array(
 *     'body'     => '<input type="text" id="name" name="name">',
 *     'label'    => 'Name',
 *     'messages' => array('validator 1 error', 'validator 2 error' )
 * )
 *
 * @package View Helpers
 * @author Tudor Barbu <miau@motane.lu>
 */

class MyLibrary_View_Helper_SplitElement {
    /**
     * returns an array containing the element's
     * body, label and error messages
     *
     * @param Zend_Form_Element $element
     * @return array
     */
    public function splitElement(Zend_Form_Element $element) {
        $result = array();
        $result['body'] = $element->getView()->{$element->helper}(
            $element->getName(),
            $element->getValue(),
            $element->getAttribs(),
            $element->options
        );

        $result['label'] = $element->getLabel();
        $result['messages'] = $element->getMessages();

        return $result;
    }
}

And now, in the controller:

class TestController extends Zend_Controller_Action {
    public function exampleAction() {
         $this->view->form = new MyForm();
         // other controller logic
    }
}

and in the view (application/views/scripts/test/example.phtml):

echo $this->form;

If you have other ideas on how to style Zend_Form objects, don’t be shy and post a comment below.