1 <?xml version="1.0" encoding="UTF-8"?>
3 <sect1 id="learning.form.decorators.layering">
4 <title>Layering Decorators</title>
7 If you were following closely in <link linkend="learning.form.decorators.simplest">the
8 previous section</link>, you may have noticed that a decorator's
9 <methodname>render()</methodname> method takes a single argument,
10 <varname>$content</varname>. This is expected to be a string.
11 <methodname>render()</methodname> will then take this string and decide to either replace
12 it, append to it, or prepend it. This allows you to have a chain of decorators -- which
13 allows you to create decorators that render only a subset of the element's metadata, and
14 then layer these decorators to build the full markup for the element.
18 Let's look at how this works in practice.
22 For most form element types, the following decorators are used:
28 <classname>ViewHelper</classname> (render the form input using one of the standard
35 <classname>Errors</classname> (render validation errors via an unordered list).
41 <classname>Description</classname> (render any description attached to the element;
42 often used for tooltips).
48 <classname>HtmlTag</classname> (wrap all of the above in a
49 <emphasis><dd></emphasis> tag.
55 <classname>Label</classname> (render the label preceding the above, wrapped in a
56 <emphasis><dt></emphasis> tag.
62 You'll notice that each of these decorators does just one thing, and operates on one
63 specific piece of metadata stored in the form element: the <classname>Errors</classname>
64 decorator pulls validation errors and renders them; the <classname>Label</classname>
65 decorator pulls just the label and renders it. This allows the individual decorators to be
66 very succinct, repeatable, and, more importantly, testable.
70 It's also where that <varname>$content</varname> argument comes into play: each decorator's
71 <methodname>render()</methodname> method is designed to accept content, and then either
72 replace it (usually by wrapping it), prepend to it, or append to it.
76 So, it's best to think of the process of decoration as one of building an onion from the
81 To simplify the process, we'll take a look at the example from <link
82 linkend="learning.form.decorators.simplest">the previous section</link>. Recall:
85 <programlisting language="php"><![CDATA[
86 class My_Decorator_SimpleInput extends Zend_Form_Decorator_Abstract
88 protected $_format = '<label for="%s">%s</label>'
89 . '<input id="%s" name="%s" type="text" value="%s"/>';
91 public function render($content)
93 $element = $this->getElement();
94 $name = htmlentities($element->getFullyQualifiedName());
95 $label = htmlentities($element->getLabel());
96 $id = htmlentities($element->getId());
97 $value = htmlentities($element->getValue());
99 $markup = sprintf($this->_format, $id, $label, $id, $name, $value);
106 Let's now remove the label functionality, and build a separate decorator for that.
109 <programlisting language="php"><![CDATA[
110 class My_Decorator_SimpleInput extends Zend_Form_Decorator_Abstract
112 protected $_format = '<input id="%s" name="%s" type="text" value="%s"/>';
114 public function render($content)
116 $element = $this->getElement();
117 $name = htmlentities($element->getFullyQualifiedName());
118 $id = htmlentities($element->getId());
119 $value = htmlentities($element->getValue());
121 $markup = sprintf($this->_format, $id, $name, $value);
126 class My_Decorator_SimpleLabel extends Zend_Form_Decorator_Abstract
128 protected $_format = '<label for="%s">%s</label>';
130 public function render($content)
132 $element = $this->getElement();
133 $id = htmlentities($element->getId());
134 $label = htmlentities($element->getLabel());
136 $markup = sprintf($this->_format, $id, $label);
143 Now, this may look all well and good, but here's the problem: as written currently, the last
144 decorator to run wins, and overwrites everything. You'll end up with just the input, or
145 just the label, depending on which you register last.
149 To overcome this, simply concatenate the passed in <varname>$content</varname> with the
153 <programlisting language="php"><![CDATA[
154 return $content . $markup;
158 The problem with the above approach comes when you want to programmatically choose whether
159 the original content should precede or append the new markup. Fortunately, there's a
160 standard mechanism for this already; <classname>Zend_Form_Decorator_Abstract</classname> has
161 a concept of placement and defines some constants for matching it. Additionally, it allows
162 specifying a separator to place between the two. Let's make use of those:
165 <programlisting language="php"><![CDATA[
166 class My_Decorator_SimpleInput extends Zend_Form_Decorator_Abstract
168 protected $_format = '<input id="%s" name="%s" type="text" value="%s"/>';
170 public function render($content)
172 $element = $this->getElement();
173 $name = htmlentities($element->getFullyQualifiedName());
174 $id = htmlentities($element->getId());
175 $value = htmlentities($element->getValue());
177 $markup = sprintf($this->_format, $id, $name, $value);
179 $placement = $this->getPlacement();
180 $separator = $this->getSeparator();
181 switch ($placement) {
183 return $markup . $separator . $content;
186 return $content . $separator . $markup;
191 class My_Decorator_SimpleLabel extends Zend_Form_Decorator_Abstract
193 protected $_format = '<label for="%s">%s</label>';
195 public function render($content)
197 $element = $this->getElement();
198 $id = htmlentities($element->getId());
199 $label = htmlentities($element->getLabel());
201 $markup = sprint($this->_format, $id, $label);
203 $placement = $this->getPlacement();
204 $separator = $this->getSeparator();
205 switch ($placement) {
207 return $markup . $separator . $content;
210 return $content . $separator . $markup;
217 Notice in the above that I'm switching the default case for each; the assumption will be
218 that labels prepend content, and input appends.
222 Now, let's create a form element that uses these:
225 <programlisting language="php"><![CDATA[
226 $element = new Zend_Form_Element('foo', array(
228 'belongsTo' => 'bar',
230 'prefixPath' => array('decorator' => array(
231 'My_Decorator' => 'path/to/decorators/',
233 'decorators' => array(
241 How will this work? When we call <methodname>render()</methodname>, the element will iterate
242 through the various attached decorators, calling <methodname>render()</methodname> on each.
243 It will pass an empty string to the very first, and then whatever content is created will be
244 passed to the next, and so on:
250 Initial content is an empty string: ''.
256 '' is passed to the <classname>SimpleInput</classname> decorator, which then
257 generates a form input that it appends to the empty string: <emphasis><input
258 id="bar-foo" name="bar[foo]" type="text" value="test"/></emphasis>.
264 The input is then passed as content to the <classname>SimpleLabel</classname>
265 decorator, which generates a label and prepends it to the original content; the
266 default separator is a <constant>PHP_EOL</constant> character, giving us this:
267 <emphasis><label for="bar-foo">\n<input id="bar-foo" name="bar[foo]"
268 type="text" value="test"/></emphasis>.
274 But wait a second! What if you wanted the label to come after the input for some reason?
275 Remember that "placement" flag? You can pass it as an option to the decorator. The easiest
276 way to do this is to pass an array of options with the decorator during element creation:
279 <programlisting language="php"><![CDATA[
280 $element = new Zend_Form_Element('foo', array(
282 'belongsTo' => 'bar',
284 'prefixPath' => array('decorator' => array(
285 'My_Decorator' => 'path/to/decorators/',
287 'decorators' => array(
289 array('SimpleLabel', array('placement' => 'append')),
295 Notice that when passing options, you must wrap the decorator within an array; this hints to
296 the constructor that options are available. The decorator name is the first element of the
297 array, and options are passed in an array to the second element of the array.
301 The above results in the markup <emphasis><input id="bar-foo" name="bar[foo]" type="text"
302 value="test"/>\n<label for="bar-foo"></emphasis>.
306 Using this technique, you can have decorators that target specific metadata of the element
307 or form and create only the markup relevant to that metadata; by using mulitiple decorators,
308 you can then build up the complete element markup. Our onion is the result.
312 There are pros and cons to this approach. First, the cons:
318 More complex to implement. You have to pay careful attention to the decorators you
319 use and what placement you utilize in order to build up the markup in the correct
326 More resource intensive. More decorators means more objects; multiply this by the
327 number of elements you have in a form, and you may end up with some serious resource
328 usage. Caching can help here.
334 The advantages are compelling, though:
340 Reusable decorators. You can create truly re-usable decorators with this technique,
341 as you don't have to worry about the complete markup, but only markup for one or a
342 few pieces of element or form metadata.
348 Ultimate flexibility. You can theoretically generate any markup combination you want
349 from a small number of decorators.
355 While the above examples are the intended usage of decorators within
356 <classname>Zend_Form</classname>, it's often hard to wrap your head around how the
357 decorators interact with one another to build the final markup. For this reason, some
358 flexibility was added in the 1.7 series to make rendering individual decorators possible --
359 which gives some Rails-like simplicity to rendering forms. We'll look at that in the next