15.5. Creating Custom Form Markup Using Zend_Form_Decorator

Rendering a form object is completely optional -- you do not need to use Zend_Form's render() methods at all. However, if you do, decorators are used to render the various form objects.

An arbitrary number of decorators may be attached to each item (elements, display groups, sub forms, or the form object itself); however, only one decorator of a given type may be attached to each item. Decorators are called in the order they are registered. Depending on the decorator, it may replace the content passed to it, or append or prepend the content.

Object state is set via configuration options passed to the constructor or the decorator's setOptions() method. When creating decorators via an item's addDecorator() or related methods, options may be passed as an argument to the method. These can be used to specify placement, a separator to use between passed in content and newly generated content, and whatever options the decorator supports.

Before each decorator's render() method is called, the current item is set in the decorator using setElement(), giving the decorator awareness of the item being rendered. This allows you to create decorators that only render specific portions of the item -- such as the label, the value, error messages, etc. By stringing together several decorators that render specific element segments, you can build complex markup representing the entire item.

15.5.1. Operation

To configure a decorator, pass an array of options or a Zend_Config object to its constructor, an array to setOptions(), or a Zend_Config object to setConfig().

Standard options include:

  • placement: Placement can be either 'append' or 'prepend' (case insensitive), and indicates whether content passed to render() will be appended or prepended, respectively. In the case that a decorator replaces the content, this setting is ignored. The default setting is to append.

  • separator: The separator is used between the content passed to render() and new content generated by the decorator, or between items rendered by the decorator (e.g. FormElements uses the separator between each item rendered). In the case that a decorator replaces the content, this setting may be ignored. The default value is PHP_EOL.

The decorator interface specifies methods for interacting with options. These include:

  • setOption($key, $value): set a single option.

  • getOption($key): retrieve a single option value.

  • getOptions(): retrieve all options.

  • removeOption($key): remove a single option.

  • clearOptions(): remove all options.

Decorators are meant to interact with the various Zend_Form class types: Zend_Form, Zend_Form_Element, Zend_Form_DisplayGroup, and all classes deriving from them. The method setElement() allows you to set the object the decorator is currently working with, and getElement() is used to retrieve it.

Each decorator's render() method accepts a string, $content. When the first decorator is called, this string is typically empty, while on subsequent calls it will be populated. Based on the type of decorator and the options passed in, the decorator will either replace this string, prepend the string, or append the string; an optional separator will be used in the latter two situations.

15.5.2. Standard Decorators

Zend_Form ships with several standard decorators.

15.5.2.1. Callback

The Callback decorator can execute an arbitrary callback to render content. Callbacks should be specified via the 'callback' option passed in the decorator configuration, and can be any valid PHP callback type. Callbacks should accept three arguments, $content (the original content passed to the decorator), $element (the item being decorated), and an array of $options. As an example callback:

<?php
class Util
{
    public static function label($content, $element, array $options)
    {
        return '<span class="label">' . $element->getLabel() . "</span>";
    }
}
?>

This callback would be specified as array('Util', 'label'), and would generate some (bad) HTML markup for the label. The Callback decorator would then either replace, append, or prepend the original content with the return value of this.

The Callback decorator allows specifying a null value for the placement option, which will replace the original content with the callback return value; 'prepend' and 'append' are still valid as well.

15.5.2.2. DtDdWrapper

The default decorators utilize definition lists (<dl>) to render form elements. Since form items can appear in any order, display groups and sub forms can be interspersed with other form items. To keep these particular item types within the definition list, the DtDdWrapper creates a new, empty definition term (<dt>) and wraps its content in a new definition datum (<dd>). The output looks something like this:

<dt></dt>
<dd><fieldset id="subform">
    <legend>User Information</legend>
    ...
</fieldset></dd>

This decorator replaces the content provided to it by wrapping it within the <dd> element.

15.5.2.3. Errors

Element errors get their own decorator with the Errors decorator. This decorator proxies to the FormErrors view helper, which renders error messages in an unordered list (<ul>) as list items. The <ul> element receives a class of "errors".

The Errors decorator can either prepend or append the content provided to it.

15.5.2.4. Fieldset

Display groups and sub forms render their content within fieldsets by default. The Fieldset decorator checks for either a 'legend' option or a getLegend() method in the registered element, and uses that as a legend if non-empty. Any content passed in is wrapped in the HTML fieldset, replacing the original content. Any attributes set in the decorated item are passed to the fieldset as HTML attributes.

15.5.2.5. Form

Zend_Form objects typically need to render an HTML form tag. The Form decorator proxies to the Form view helper. It wraps any provided content in an HTML form element, using the Zend_Form object's action and method, and any attributes as HTML attributes.

15.5.2.6. FormElements

Forms, display groups, and sub forms are collections of elements. In order to render these elements, they utilize the FormElements decorator, which iterates through all items, calling render() on each and joining them with the registered separator. It can either append or prepend content passed to it.

15.5.2.7. HtmlTag

The HtmlTag decorator allows you to utilize HTML tags to decorate content; the tag utilized is passed in the 'tag' option, and any other options are used as HTML attributes to that tag. The tag by default is assumed to be block level, and replaces the content by wrapping it in the given tag. However, you can specify a placement to append or prepend a tag as well.

15.5.2.8. Label

Form elements typically have labels, and the Label decorator is used to render these labels. It proxies to the FormLabel view helper, and pulls the element label using the getLabel() method of the element. If no label is present, none is rendered.

You may optionally specify a 'tag' option; if provided, it wraps the label in that block-level tag. If the 'tag' option is present, and no label present, the tag is rendered with no content.

By default, the Label decorator prepends to the provided content; specify a 'placement' option of 'append' to place it after the content.

15.5.2.9. ViewHelper

Most elements utilize Zend_View helpers for rendering, and this is done with the ViewHelper decorator. With it, you may specify a 'helper' tag to explicitly set the view helper to utilize; if none is provided, it uses the last segment of the element's class name to determine the helper, prepending it with the string 'form': e.g., 'Zend_Form_Element_Text' would look for a view helper of 'formText'.

Any attributes of the provided element are passed to the view helper as element attributes.

By default, this decorator appends content; use the 'placement' option to specify alternate placement.

15.5.3. Custom Decorators

If you find your rendering needs are complex or need heavy customization, you should consider creating a custom decorator.

Decorators need only implement Zend_Decorator_Interface. The interface specifies the following:

<?php
interface Zend_Decorator_Interface
{
    public function __construct($options = null);
    public function setElement($element);
    public function getElement();
    public function setOptions(array $options);
    public function setConfig(Zend_Config $config);
    public function setOption($key, $value);
    public function getOption($key);
    public function getOptions();
    public function removeOption($key);
    public function clearOptions();
    public function render($content);
}
?>

To make this simpler, you can simply extend Zend_Decorator_Abstract, which implements all methods except render().

As an example, if you wanted to have labels append a ':', and also display a '*' when required, you could write a decorator like the following:

<?php
class My_Decorator_Label extends Zend_Form_Decorator_Abstract
{
    public function getLabel()
    {
        $label = $this->getOption('label');
        if (null === $label) {
            if ((null !== ($element = $this->getElement()))
                && ($element instanceof Zend_Form_Element) )
            {
                $label = $element->getLabel();
            } 
        }
        
        return (string) $label;
    }

    public function render($content)
    {
        $element = $this->getElement();
        if (!$element instanceof Zend_Form_Element) {
            return $content;
        }

        $label = $this->getLabel();
        if ($translator = $element->getTranslator()) {
            $label = $translator->translate($label);
        }
        if ($element->getRequired()) {
            $label .= '*';
        }
        $label .= ':';

        $separator = $this->getSeparator();
        $placement = $this->getPlacement();
        $view      = $element->getView();
        if (null !== $view) {
            $label = $view->formLabel($element->getName(), $label, $this->getOptions());
        }

        switch ($placement) {
            case (self::PREPEND):
                return $label . $separator . $content;
            case (self::APPEND):
            default:
                return $content . $separator . $label;
        }
    }
}
?>

You can then place this in the decorator path:

<?php
// for an element:
$element->addPrefixPath('My_Decorator', 'My/Decorator/', 'decorator');

// for all elements:
$form->addElementPrefixPath('My_Decorator', 'My/Decorator/', 'decorator');
?>

In this particular example, because the decorator's final segment, 'Label' matches the same as Zend_Form_Decorator_Label, it will be rendered in place of that decorator -- meaning you would not need to change any decorators to modify the output. (Needless to say, you can create decorators with different names; this simply shows how you can quickly and simply override existing rendering functionality through custom decorators.)