Decorators are hard; let’s go shopping!

2009 February 23
by monzee

Decorators are a pain. To make a Zend_Form appear exactly as you want it to, you’ll have to fight with decorators and its cryptic, unintuitive syntax. There are many blog posts and tutorials extolling the flexibility and power of decorators, but they’re not being completely honest. They don’t mention how many hoops you’ll have to jump through to make it bend to your will.

The main value of decorators lies in its ability to pull data from the elements and feed them to various view helpers to generate the markup. It allows you to write <?php echo $this->form; ?> in the view and get the whole markup automatically. It is very convenient, but this is too sugary and magical for my tastes. Also, you would have to whack your brains around a bit to figure out the correct sequence of decorators that would generate your desired markup. And when you need to write a different set of decorators for each type (or worse, each element), you’ll begin to wonder if you’d be better off writing out the HTML in the view instead.

The pain

For example, hidden elements. I don’t want a hidden element to be wrapped in a <dd> (or <li>, or <td>) as it would take up unnecessary space in the page. I typically clump together my hidden elements right after the opening <form> tag or right before the closing one. Theoretically, this can be achieved by stripping the decorators of all hidden elements to just the ViewHelper, adding them to a display group, stripping down the display group decorators to FormElements, then adjusting the form decorators again to make the group appear outside the <dt>, <ul> or <table> wrapper (bare elements inside these tags will make the HTML invalid).

The last part is the most troublesome, as you can’t tell the form to render this group first, then the wrapper open tag, then the rest of the elements, then the wrapper close tag, and finally wrap them all with the form tag. In fact, I haven’t achieved this sort of setup without resorting to the ViewScript decorator (which is basically just a view partial, and hence written as if you’re not using decorators at all) or overriding the form render() method.

The other extreme

On the other hand, laying out HTML manually is much worse. For every form element, you’ll have to write something like this:

<input type="text" name="<?php echo $this->form->foo->getName(); ?>"
id="<?php echo $this->form->foo->getId(); ?>" value="<?php echo $this->form->foo->getValue();"
<?php if ($class = $this->form->foo->getAttrib('class')) : ?> class="<?php echo $class; ?>"<?php endif; ?> />

That’s just the element. You also have to do similar data pulling for the labels and errors (and descriptions if you use them). I don’t even want to imagine writing that stuff for selects or radio groups.

Thankfully for us, there are view helpers for generating markup like this.

<?php echo $this->formLabel($this->form->foo->getName(), $this->form->foo->getLabel()); ?>
<?php echo $this->formText($this->form->foo->getName(), $this->form->foo->getValue(), $this->form->foo->getAttribs()); ?>
<?php echo $this->formErrors($this->form->foo->getErrors()); ?>

That is much better. Now you can just insert tags here and there and you get your ideal markup. But pulling all these data from each element is tedious. Wouldn’t it be nice if we could just call an element method that pulls all these data automatically and feeds them to these helpers? That’s exactly what decorators do!

The middle ground

Instead of rendering whole forms, we could render individual elements and place them in our view. Something like this: (I’ll be using short tags from hereon; this is not recommended because it isn’t guaranteed to work anywhere.)

<form method="<?= $this->form->getMethod(); ?>" action="<?= $this->form->getAction(); ?>" enctype="<?= $this->form->getEncType(); ?>">
<ul>
    <li><?= $this->form->foo; ?></li>
    <li><?= $this->form->bar; ?></li>
    <li><?= $this->form->submit; ?></li>
</ul>
<?= $this->form->secret; ?>
</form>

(I didn’t use the form view helper here because only the explicitly declared attributes are available in $form->getAttribs(). I.e., if you don’t declare the method or enctype and rely on the default values, you won’t get those default values from getAttribs().)

Assuming you set the decorators and element properties properly, you will end up with:

<form method="post" action="/path/to/submit" enctype="application/x-www-form-urlencoded">
<ul>
    <li>
        <label for="foo">Foo:</label><br />
        <input type="text" id="foo" name="foo" class="text" />
        <ul class="errors"><li>some error</li></ul> <!-- if you have errors -->
    </li>
    <li>
        <label for="bar">Bar:</label><br />
        <textarea id="bar" name="bar" class="text">Default value</textarea>
    </li>
    <li><input type="submit" id="submit" value="Submit" /></li>
</ul>
<input type="hidden" id="secret" name="secret" value="secret" />
</form>

Now there’s a fairly good balance of magic and verbosity. The decorators needed to generate these markup are also pretty straightforward. Figuring them out is left as an exercise to the reader ;) .

What if I want a fairly complex markup between the label and input instead of a simple <br />? For example, I need to put a nested table with dynamic content, pictures and what-not right between the foo label and input tags. I could probably do that with a decorator, but decorators are hard and writing straight-up HTML requires no thinking.

Rendering individual decorators

This is a fairly recent addition to Zend_Form. I believe this was added around version 1.6. Given a form element, we could render its decorators separately using the syntax $form->element->renderDecorator() where Decorator is the name or alias of the desired decorator. For example, if we want to render the foo label only, we could call <?= $this->form->foo->renderLabel(); ?> and get just that. Calls of this form are captured by the catch-all magic method, so you won’t find this in the API docs without looking deeply. It is mentioned in the manual but not explored nearly enough.

By using this feature, we could do the same thing illustrated in the latter part of the third section of this post but in a much more terse and descriptive manner. This is how I typically use Zend_Form nowadays:

<?php
class FooForm extends Zend_Form
{
    public function init()
    {
        $decors = array(
            array(array('Elem' => 'ViewHelper'), array('separator' => '')), // i reset the separators to get rid
            array('Label', array('separator' => '')),                       // those unpredictable newlines
            array('Errors', array('separator' => '')),                      // in the render output
        );

        $this->addElement('text', 'foo', array(
            'class' => 'text',
            'label' => 'Foo:',
            'required' => true,
            'decorators' => $decors,
        ));

        $this->addElement('textarea', 'bar', array(
            'class' => 'text',
            'label' => 'Bar:',
            'required' => true,
            'decorators' => $decors,
        ));

        $this->addElement('hidden', 'secret', array(
            'value' => 'secret',
            'decorators' => $decors,
        ));

        $this->addElement('submit', 'submit', array(
            'class' => 'button',
            'label' => 'Submit',
            'ignore' => true,
            'decorators' => $decors,
        ));
    }
}

Then in the view:

<?= $this->form->renderForm(false); ?> <!-- without the param, the close tag will also be rendered -->
<ul>
    <li>
        <?= $this->form->foo->renderLabel(); ?>
        <!-- do the crazy markup here -->
        <?= $this->form->foo->renderElem(); ?><br />
        <!-- if i didn't alias ViewHelper as Elem above, this should be called with renderViewHelper() instead -->
        <?= $this->form->foo->renderErrors(); ?>
    </li>
    <li>
        <?= $this->form->bar->renderLabel(); ?>
        <?= $this->form->bar->renderElem(); ?><br />
        <?= $this->form->bar->renderErrors(); ?>
    </li>
    <li><?= $this->form->submit->renderElem(); ?></li>
</ul>
<?= $this->form->secret->renderElem(); ?>
</form>

I don’t use this style exclusively; if I don’t need special markup and the decorators are straightforward, I also render whole elements in some places.

Caveat

Your file elements would not work if you do this. They need special decorators to work properly, specifically the File decorator. For the file element, it’s a simple case of switching the ViewHelper with File, but other elements might need their own decorators and it might not be as simple. The captcha element especially is a weird one. It adds its own decorators only when its render() method is called. You could try mimicking the captcha render() logic inside your form class, but I generally leave them alone and render captchas whole. I have no experience with Dojo or jQuery form elements, but they would also likely fail. I’m not a fan of writing Javascript in PHP (or any other language except Javascript) anyway.
Update: even something as seemingly basic as the checkbox element breaks using this method. I haven’t looked into the other basic elements yet, but I will once I sort my PC problems. But the rule of thumb is that any element with special logic in their render() method will break if they are rendered piecemeal, as well as any element that requires special decorators like File or Captcha (although they probably will still work fine if the required decorator is added and rendered in the view as in the case of the file element).

Conclusion

Decorators are convenient, but troublesome to get around with. Luckily, it’s not an all-or-nothing deal; we are not required to use them as designed, but that doesn’t mean that we would be writing HTML and pulling data manually if choose not to. It’s up to you to find the sweet spot between manual HTML and decorators.

One Response leave one →

Trackbacks & Pingbacks

  1. Stock decorators « mzBlog

Leave a Reply

Note: You can use basic XHTML in your comments. Your email address will never be published.

Subscribe to this comment feed via RSS