1 <?xml version="1.0" encoding="UTF-8"?>
3 <sect1 id="learning.form.decorators.composite">
4 <title>Creating and Rendering Composite Elements</title>
7 In <link linkend="learning.form.decorators.individual">the last section</link>, we had an
8 example showing a "date of birth element":
11 <programlisting language="php"><![CDATA[
13 <?php echo $form->dateOfBirth->renderLabel() ?>
14 <?php echo $this->formText('dateOfBirth[day]', '', array(
15 'size' => 2, 'maxlength' => 2)) ?>
17 <?php echo $this->formText('dateOfBirth[month]', '', array(
18 'size' => 2, 'maxlength' => 2)) ?>
20 <?php echo $this->formText('dateOfBirth[year]', '', array(
21 'size' => 4, 'maxlength' => 4)) ?>
26 How might you represent this element as a <classname>Zend_Form_Element</classname>?
27 How might you write a decorator to render it?
30 <sect2 id="learning.form.decorators.composite.element">
31 <title>The Element</title>
34 The questions about how the element would work include:
40 How would you set and retrieve the value?
46 How would you validate the value?
52 Regardless, how would you then allow for discrete form inputs for the three
53 segments (day, month, year)?
59 The first two questions center around the form element itself: how would
60 <methodname>setValue()</methodname> and <methodname>getValue()</methodname> work?
61 There's actually another question implied by the question about the decorator: how would
62 you retrieve the discrete date segments from the element and/or set them?
66 The solution is to override the <methodname>setValue()</methodname> method of your
67 element to provide some custom logic. In this particular case, our element should have
68 three discrete behaviors:
74 If an integer timestamp is provided, it should be used to determine and store
75 the day, month, and year.
81 If a textual string is provided, it should be cast to a timestamp, and then that
82 value used to determine and store the day, month, and year.
88 If an array containing keys for date, month, and year is provided, those values
95 Internally, the day, month, and year will be stored discretely. When the value of the
96 element is retrieved, it will be done so in a normalized string format. We'll override
97 <methodname>getValue()</methodname> as well to assemble the discrete date segments into
102 Here's what the class would look like:
105 <programlisting language="php"><![CDATA[
106 class My_Form_Element_Date extends Zend_Form_Element_Xhtml
108 protected $_dateFormat = '%year%-%month%-%day%';
113 public function setDay($value)
115 $this->_day = (int) $value;
119 public function getDay()
124 public function setMonth($value)
126 $this->_month = (int) $value;
130 public function getMonth()
132 return $this->_month;
135 public function setYear($value)
137 $this->_year = (int) $value;
141 public function getYear()
146 public function setValue($value)
148 if (is_int($value)) {
149 $this->setDay(date('d', $value))
150 ->setMonth(date('m', $value))
151 ->setYear(date('Y', $value));
152 } elseif (is_string($value)) {
153 $date = strtotime($value);
154 $this->setDay(date('d', $date))
155 ->setMonth(date('m', $date))
156 ->setYear(date('Y', $date));
157 } elseif (is_array($value)
158 && (isset($value['day'])
159 && isset($value['month'])
160 && isset($value['year'])
163 $this->setDay($value['day'])
164 ->setMonth($value['month'])
165 ->setYear($value['year']);
167 throw new Exception('Invalid date value provided');
173 public function getValue()
176 array('%year%', '%month%', '%day%'),
177 array($this->getYear(), $this->getMonth(), $this->getDay()),
185 This class gives some nice flexibility -- we can set default values from our database,
186 and be certain that the value will be stored and represented correctly. Additionally,
187 we can allow for the value to be set from an array passed via form input. Finally, we
188 have discrete accessors for each date segment, which we can now use in a decorator to
189 create a composite element.
193 <sect2 id="learning.form.decorators.composite.decorator">
194 <title>The Decorator</title>
197 Revisiting the example from the last section, let's assume that we want users to input
198 each of the year, month, and day separately. <acronym>PHP</acronym> fortunately allows
199 us to use array notation when creating elements, so it's still possible to capture these
200 three entities into a single value -- and we've now created a
201 <classname>Zend_Form</classname> element that can handle such an array value.
205 The decorator is relatively simple: it will grab the day, month, and year from the
206 element, and pass each to a discrete view helper to render individual form inputs; these
207 will then be aggregated to form the final markup.
210 <programlisting language="php"><![CDATA[
211 class My_Form_Decorator_Date extends Zend_Form_Decorator_Abstract
213 public function render($content)
215 $element = $this->getElement();
216 if (!$element instanceof My_Form_Element_Date) {
217 // only want to render Date elements
221 $view = $element->getView();
222 if (!$view instanceof Zend_View_Interface) {
223 // using view helpers, so do nothing if no view present
227 $day = $element->getDay();
228 $month = $element->getMonth();
229 $year = $element->getYear();
230 $name = $element->getFullyQualifiedName();
241 $markup = $view->formText($name . '[day]', $day, $params)
242 . ' / ' . $view->formText($name . '[month]', $month, $params)
243 . ' / ' . $view->formText($name . '[year]', $year, $yearParams);
245 switch ($this->getPlacement()) {
247 return $markup . $this->getSeparator() . $content;
250 return $content . $this->getSeparator() . $markup;
257 We now have to do a minor tweak to our form element, and tell it that we want to use the
258 above decorator as a default. That takes two steps. First, we need to inform the element of
259 the decorator path. We can do that in the constructor:
262 <programlisting language="php"><![CDATA[
263 class My_Form_Element_Date extends Zend_Form_Element_Xhtml
267 public function __construct($spec, $options = null)
269 $this->addPrefixPath(
274 parent::__construct($spec, $options);
282 Note that this is being done in the constructor and not in <methodname>init()</methodname>.
283 This is for two reasons. First, it allows extending the element later to add logic in
284 <methodname>init</methodname> without needing to worry about calling
285 <methodname>parent::init()</methodname>. Second, it allows passing additional plugin paths
286 via configuration or within an <methodname>init</methodname> method that will then allow
287 overriding the default <classname>Date</classname> decorator with my own replacement.
291 Next, we need to override the <methodname>loadDefaultDecorators()</methodname> method to use
292 our new <classname>Date</classname> decorator:
295 <programlisting language="php"><![CDATA[
296 class My_Form_Element_Date extends Zend_Form_Element_Xhtml
300 public function loadDefaultDecorators()
302 if ($this->loadDefaultDecoratorsIsDisabled()) {
306 $decorators = $this->getDecorators();
307 if (empty($decorators)) {
308 $this->addDecorator('Date')
309 ->addDecorator('Errors')
310 ->addDecorator('Description', array(
312 'class' => 'description'
314 ->addDecorator('HtmlTag', array(
316 'id' => $this->getName() . '-element'
318 ->addDecorator('Label', array('tag' => 'dt'));
327 What does the final output look like? Let's consider the following element:
330 <programlisting language="php"><![CDATA[
331 $d = new My_Form_Element_Date('dateOfBirth');
332 $d->setLabel('Date of Birth: ')
333 ->setView(new Zend_View());
335 // These are equivalent:
336 $d->setValue('20 April 2009');
337 $d->setValue(array('year' => '2009', 'month' => '04', 'day' => '20'));
341 If you then echo this element, you get the following markup (with some slight whitespace
342 modifications for readability):
345 <programlisting language="html"><![CDATA[
346 <dt id="dateOfBirth-label"><label for="dateOfBirth" class="optional">
349 <dd id="dateOfBirth-element">
350 <input type="text" name="dateOfBirth[day]" id="dateOfBirth-day"
351 value="20" size="2" maxlength="2"> /
352 <input type="text" name="dateOfBirth[month]" id="dateOfBirth-month"
353 value="4" size="2" maxlength="2"> /
354 <input type="text" name="dateOfBirth[year]" id="dateOfBirth-year"
355 value="2009" size="4" maxlength="4">
360 <sect2 id="learning.form.decorators.composite.conclusion">
361 <title>Conclusion</title>
364 We now have an element that can render multiple related form input fields, and then
365 handle the aggregated fields as a single entity -- the <varname>dateOfBirth</varname>
366 element will be passed as an array to the element, and the element will then, as we
367 noted earlier, create the appropriate date segments and return a value we can use for
372 Additionally, we can use different decorators with the element. If we wanted to use a
373 <ulink url="http://dojotoolkit.org/">Dojo</ulink> <classname>DateTextBox</classname>
374 dijit decorator -- which accepts and returns string values -- we can, with no
375 modifications to the element itself.
379 In the end, you get a uniform element <acronym>API</acronym> you can use to describe an
380 element representing a composite value.