A Zend Framwork compound form element for dates
A while ago I needed to ask a user for their date of birth on a Zend_Form. The design showed three separate select elements to do this:
A little bit of googling found this site http://codecaine.co.za/posts/compound-elements-with-zend-form which has now unfortunately disappeared, so the code in this article owes a lot of the author of that article.
It turns out to be remarkably simple to create a single Zend Form element that is rendered as multiple form elements. We create an element object and a view helper object and we’re done. Usage then looks like:
<?php class Application_Form_Details extends Zend_Form { public function init() { $this->addPrefixPath('App_Form', 'App/Form/'); // other elements before $this->addElement('date', 'date_of_birth', array( 'label' => 'Date of birth:' )); // other elements after $this->addElement('submit', 'Go'); } }
Obviously, this form lives in application/forms/Detail.php and is rendered as usual in a view script. In our form definition, we have added an element called ‘date’ and with the addition of the addPrefixPath call have told the form that in addition to using the standard Zend Framework form elements, also look in library/App/Form. (Incidentally, we can also now override any supplied form element by simply dropping a replacement into the libraryApp/Form folder.)
The date form element lives in library/App/Form/Element/Date.php as Zend_Form knows to look in a subfolder for App/Form called Elements for any element objects and will look in the Decorator/ sub folder for decorator objects.
The Date element looks like this:
// Based on code from // http://codecaine.co.za/posts/compound-elements-with-zend-form class App_Form_Element_Date extends Zend_Form_Element_Xhtml { public $helper = 'formDate'; public function isValid ($value, $context = null) { if (is_array($value)) { $value = $value['year'] . '-' . $value['month'] . '-' . $value['day']; if($value == '--') { $value = null; } } return parent::isValid($value, $context); } public function getValue() { if(is_array($this->_value)) { $value = $this->_value['year'] . '-' . $this->_value['month'] . '-' . $this->_value['day']; if($value == '--') { $value = null; } $this->setValue($value); } return parent::getValue(); } }
There’s quite a lot going on here, but it should be fairly clear. Firstly we specify the name of the view helper to use when rendering this element to be formDate which we will write. We know this element is going to consist of three select boxes and so these will end up being an array of posted data for day, month and year. As a result, we need to override isValid() to turn our array back into a string and then call up to the parent’s isValid() in order to do the actual validation required. We also need to override getValue() in the same way to ensure that it is also a string. Again we call up to the parent’s getValue() as that does filtering.
That’s all there is to the element itself, so now we turn out attention to the view helper that will render the element. This lives in library/App/View/Helpers/FormDate.php and as per my view helpers post, we need to tell the view about that folder via application.ini:
autoloadernamespaces[] = "App_"
resources.view.helperPath.App_View_Helper = "App/View/Helper"
The formDate view helper code looks like this:
<?php // based on code from // http://codecaine.co.za/posts/compound-elements-with-zend-form class App_View_Helper_FormDate extends Zend_View_Helper_FormElement { public function formDate ($name, $value = null, $attribs = null) { // separate value into day, month and year $day = ''; $month = ''; $year = ''; if (is_array($value)) { $day = $value['day']; $month = $value['month']; $year = $value['year']; } elseif (strtotime($value)) { list($year, $month, $day) = explode('-', date('Y-m-d', strtotime($value))); } // build select options $dayAttribs = isset($attribs['dayAttribs']) ? $attribs['dayAttribs'] : array(); $monthAttribs = isset($attribs['monthAttribs']) ? $attribs['monthAttribs'] : array(); $yearAttribs = isset($attribs['yearAttribs']) ? $attribs['yearAttribs'] : array(); $dayMultiOptions = array('' => ''); for ($i = 1; $i < 32; $i ++) { $index = str_pad($i, 2, '0', STR_PAD_LEFT); $dayMultiOptions[$index] = str_pad($i, 2, '0', STR_PAD_LEFT); } $monthMultiOptions = array('' => ''); for ($i = 1; $i < 13; $i ++) { $index = str_pad($i, 2, '0', STR_PAD_LEFT); $monthMultiOptions[$index] = date('F', mktime(null, null, null, $i, 01)); } $startYear = date('Y'); if (isset($attribs['startYear'])) { $startYear = $attribs['startYear']; unset($attribs['startYear']); } $stopYear = $startYear + 10; if (isset($attribs['stopYear'])) { $stopYear = $attribs['stopYear']; unset($attribs['stopYear']); } $yearMultiOptions = array('' => ''); if ($stopYear < $startYear) { for ($i = $startYear; $i >= $stopYear; $i--) { $yearMultiOptions[$i] = $i; } } else { for ($i = $startYear; $i <= $stopYear; $i++) { $yearMultiOptions[$i] = $i; } } // return the 3 selects separated by return $this->view->formSelect( $name . '[day]', $day, $dayAttribs, $dayMultiOptions) . ' ' . $this->view->formSelect( $name . '[month]', $month, $monthAttribs, $monthMultiOptions) . ' ' . $this->view->formSelect( $name . '[year]', $year, $yearAttribs, $yearMultiOptions ); } }
Again, there’s a fair amount of code, but I expect that it’s pretty self-explanatory. Essentially, we have a lot of set up in order to render three select boxes; one for the day, month and year. I decided to use month names but it’s easy enough to change to numbers. In terms of configuration, you need to be able to specify the start and stop year numbers. These are then passed in as options when calling addElement.
That’s about it. Two separate files is all you need to create a Zend_Form element object that is implemented via compound elements. It also follows that you can do the same for any other conceptual element that you want to be rendered as multiple elements in the page.
This is very similar to a proof of concept posted by Matthew Weier O'Phinney to his blog, http://weierophinney.net/matthew/archives/217-Creating-composite-elements.html.
He uses a decorator and the already existing formText view helpers to generate the output. Obviously your method of using select lists is more user friendly and easier to validate.
I know there are many ways to skin a cat, but in you opinion do you think it is better to create your own view helpers or reuse the existing ones?
Clearly, I missed Matthew's post :) I don't think it matters which approach you take really. I like to keep my elements as close to the way other elements work as much as possible.
Regards,
Rob…
Just a bit of a type. In the last paragraph before the code autoloadernamespaces[] = "App_", you said "This lives in library/App/View/Helpers/FormDate.php" I think you meant "This lives in library/App/View/Helper/FormDate.php"?