[GENERIC] Zend_Translate:
[zend.git] / documentation / manual / en / tutorials / form-decorators-composite.xml
blob8d636f349f96e81a645fda0fa969d2e6ff7676e5
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!-- Reviewed: no -->
3 <sect1 id="learning.form.decorators.composite">
4     <title>Creating and Rendering Composite Elements</title>
6     <para>
7         In <link linkend="learning.form.decorators.individual">the last section</link>, we had an
8         example showing a "date of birth element":
9     </para>
11     <programlisting language="php"><![CDATA[
12 <div class="element">
13     <?php echo $form->dateOfBirth->renderLabel() ?>
14     <?php echo $this->formText('dateOfBirth[day]', '', array(
15         'size' => 2, 'maxlength' => 2)) ?>
16     /
17     <?php echo $this->formText('dateOfBirth[month]', '', array(
18         'size' => 2, 'maxlength' => 2)) ?>
19     /
20     <?php echo $this->formText('dateOfBirth[year]', '', array(
21         'size' => 4, 'maxlength' => 4)) ?>
22 </div>
23 ]]></programlisting>
25     <para>
26         How might you represent this element as a <classname>Zend_Form_Element</classname>?
27         How might you write a decorator to render it?
28     </para>
30     <sect2 id="learning.form.decorators.composite.element">
31         <title>The Element</title>
33         <para>
34             The questions about how the element would work include:
35         </para>
37         <itemizedlist>
38             <listitem>
39                 <para>
40                     How would you set and retrieve the value?
41                 </para>
42             </listitem>
44             <listitem>
45                 <para>
46                     How would you validate the value?
47                 </para>
48             </listitem>
50             <listitem>
51                 <para>
52                     Regardless, how would you then allow for discrete form inputs for the three
53                     segments (day, month, year)?
54                 </para>
55             </listitem>
56         </itemizedlist>
58         <para>
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?
63         </para>
65         <para>
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:
69         </para>
71         <itemizedlist>
72             <listitem>
73                 <para>
74                     If an integer timestamp is provided, it should be used to determine and store
75                     the day, month, and year.
76                 </para>
77             </listitem>
79             <listitem>
80                 <para>
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.
83                 </para>
84             </listitem>
86             <listitem>
87                 <para>
88                     If an array containing keys for date, month, and year is provided, those values
89                     should be stored.
90                 </para>
91             </listitem>
92         </itemizedlist>
94         <para>
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
98             a final string.
99         </para>
101         <para>
102             Here's what the class would look like:
103         </para>
105         <programlisting language="php"><![CDATA[
106 class My_Form_Element_Date extends Zend_Form_Element_Xhtml
108     protected $_dateFormat = '%year%-%month%-%day%';
109     protected $_day;
110     protected $_month;
111     protected $_year;
113     public function setDay($value)
114     {
115         $this->_day = (int) $value;
116         return $this;
117     }
119     public function getDay()
120     {
121         return $this->_day;
122     }
124     public function setMonth($value)
125     {
126         $this->_month = (int) $value;
127         return $this;
128     }
130     public function getMonth()
131     {
132         return $this->_month;
133     }
135     public function setYear($value)
136     {
137         $this->_year = (int) $value;
138         return $this;
139     }
141     public function getYear()
142     {
143         return $this->_year;
144     }
146     public function setValue($value)
147     {
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'])
161                   )
162         ) {
163             $this->setDay($value['day'])
164                  ->setMonth($value['month'])
165                  ->setYear($value['year']);
166         } else {
167             throw new Exception('Invalid date value provided');
168         }
170         return $this;
171     }
173     public function getValue()
174     {
175         return str_replace(
176             array('%year%', '%month%', '%day%'),
177             array($this->getYear(), $this->getMonth(), $this->getDay()),
178             $this->_dateFormat
179         );
180     }
182 ]]></programlisting>
184         <para>
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.
190         </para>
191     </sect2>
193     <sect2 id="learning.form.decorators.composite.decorator">
194         <title>The Decorator</title>
196         <para>
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.
202         </para>
204         <para>
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.
208         </para>
210         <programlisting language="php"><![CDATA[
211 class My_Form_Decorator_Date extends Zend_Form_Decorator_Abstract
213     public function render($content)
214     {
215         $element = $this->getElement();
216         if (!$element instanceof My_Form_Element_Date) {
217             // only want to render Date elements
218             return $content;
219         }
221         $view = $element->getView();
222         if (!$view instanceof Zend_View_Interface) {
223             // using view helpers, so do nothing if no view present
224             return $content;
225         }
227         $day   = $element->getDay();
228         $month = $element->getMonth();
229         $year  = $element->getYear();
230         $name  = $element->getFullyQualifiedName();
232         $params = array(
233             'size'      => 2,
234             'maxlength' => 2,
235         );
236         $yearParams = array(
237             'size'      => 4,
238             'maxlength' => 4,
239         );
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()) {
246             case self::PREPEND:
247                 return $markup . $this->getSeparator() . $content;
248             case self::APPEND:
249             default:
250                 return $content . $this->getSeparator() . $markup;
251         }
252     }
254 ]]></programlisting>
256     <para>
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:
260     </para>
262     <programlisting language="php"><![CDATA[
263 class My_Form_Element_Date extends Zend_Form_Element_Xhtml
265     // ...
267     public function __construct($spec, $options = null)
268     {
269         $this->addPrefixPath(
270             'My_Form_Decorator',
271             'My/Form/Decorator',
272             'decorator'
273         );
274         parent::__construct($spec, $options);
275     }
277     // ...
279 ]]></programlisting>
281     <para>
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.
288     </para>
290     <para>
291         Next, we need to override the <methodname>loadDefaultDecorators()</methodname> method to use
292         our new <classname>Date</classname> decorator:
293     </para>
295     <programlisting language="php"><![CDATA[
296 class My_Form_Element_Date extends Zend_Form_Element_Xhtml
298     // ...
300     public function loadDefaultDecorators()
301     {
302         if ($this->loadDefaultDecoratorsIsDisabled()) {
303             return;
304         }
306         $decorators = $this->getDecorators();
307         if (empty($decorators)) {
308             $this->addDecorator('Date')
309                  ->addDecorator('Errors')
310                  ->addDecorator('Description', array(
311                      'tag'   => 'p',
312                      'class' => 'description'
313                  ))
314                  ->addDecorator('HtmlTag', array(
315                      'tag' => 'dd',
316                      'id'  => $this->getName() . '-element'
317                  ))
318                  ->addDecorator('Label', array('tag' => 'dt'));
319         }
320     }
322     // ...
324 ]]></programlisting>
326     <para>
327         What does the final output look like? Let's consider the following element:
328     </para>
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'));
338 ]]></programlisting>
340     <para>
341         If you then echo this element, you get the following markup (with some slight whitespace
342         modifications for readability):
343     </para>
345     <programlisting language="html"><![CDATA[
346 <dt id="dateOfBirth-label"><label for="dateOfBirth" class="optional">
347     Date of Birth:
348 </label></dt>
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">
356 </dd>
357 ]]></programlisting>
358     </sect2>
360     <sect2 id="learning.form.decorators.composite.conclusion">
361         <title>Conclusion</title>
363         <para>
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
368             most backends.
369         </para>
371         <para>
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.
376         </para>
378         <para>
379             In the end, you get a uniform element <acronym>API</acronym> you can use to describe an
380             element representing a composite value.
381         </para>
382     </sect2>
383 </sect1>