Jun 17, 2008 4:51:57 PM
Zend_Form can save you a lot of time. It almost completely abstracts away the most boring and error-prone aspects of developing secure and standards-compliant HTML forms. But one thing it doesn't do out of the box is compound elements, such as three-field dates. In this post I'll show you the easiest way to do this ZF-style.
Setting the scene
The best place to start is always at the end, so let's begin with a simple use-case for a date field. Consider this very simple controller, which sets up a form and processes it.
<?php
class IndexController extends App_Controller_Action
{
public function indexAction ()
{
// create the form
$form = new Zend_Form();
$form->setAction('/');
$form->setMethod('post');
// create the date field
$date = new App_Form_Element_Date('date');
$date->setLabel('Date');
$date->setRequired(true);
// create the submit button
$submit = new Zend_Form_Element_Submit('submit');
$submit->setLabel('Submit');
// add the elements to the form
$form->addElements(array($date, $submit));
// process the form if the request is a POST
if ($this->getRequest()->isPost())
{
if ($form->isValid($_POST))
{
var_dump($form->getValues());
die;
}
}
// stick the form into the view
$this->view->form = $form;
}
}
And a view to render the form:
<?php echo $this->form ?>
The problem, of course, is that there is no App_Form_Element_Date. But if there was, this is pretty much how we'd expect it to behave: we can add it to a form like any other field, and it gets validated along with the other fields in the form. The problem, though, is that a) Zend_Form elements typically correspond to exactly one HTML form element, such as an INPUT or a SELECT, and our date field has three, and b) we've added a field to the form called 'date', but in our HTML output, there will only be date_day, date_month and date_year.
The challenge is to tie the date parts together when the form is submitted, to process the date as a unit, and then to output the date in three parts again if there is a form error or the form needs to be redisplayed.
Getting our element to render
The first step in tackling this is to get our new date field to render. To do this, we need two things: firstly, an element class to encapsulate business logic; and secondly, a view renderer to encapsulate presentation.
// element
<?php
class App_Form_Element_Date extends Zend_Form_Element_Xhtml
{
public $helper = 'formDate';
}
// view helper
<?php
class App_View_Helper_FormDate extends Zend_View_Helper_FormElement
{
public function formDate ($name, $value = null, $attribs = null)
{
// if the element is rendered without a value,
// show today's date
if ($value === null)
{
$value = date('Y-m-d');
}
list($year, $month, $day) = split('-', $value);
// build select options
$date = new Zend_Date();
$dayOptions = array();
for ($i = 1; $i < 32; $i ++)
{
$idx = str_pad($i, 2, '0', STR_PAD_LEFT);
$dayOptions[$idx] = str_pad($i, 2, '0', STR_PAD_LEFT);
}
$monthOptions = array();
for ($i = 1; $i < 13; $i ++)
{
$date->set($i, Zend_Date::MONTH);
$idx = str_pad($i, 2, '0', STR_PAD_LEFT);
$monthOptions[$idx] = $date->toString('MMMM');
}
$yearOptions = array();
for ($i = 1970; $i < 2031; $i ++)
{
$yearOptions[$i] = $i;
}
// return the 3 selects separated by -
return
$this->view->formSelect(
$name . '_day',
$day,
$attribs,
$dayOptions) . ' - ' .
$this->view->formSelect(
$name . '_month',
$month,
$attribs,
$monthOptions) . ' - ' .
$this->view->formSelect(
$name . '_year',
$year,
$attribs,
$yearOptions
);
}
}
The element class is quite simple. All it's doing is declaring that the class 'Zend_View_Helper_FormDate' should be used to render it.
The view helper is a little longer in code, but no more complex. All it's doing is generating three arrays for all the possible date options ([1..31] [1..12] [1970..2030]), and then rendering three select boxes with those options. The choice of year range in this case is arbitrary. I have not used Zend_Date to filter the date value passed, because Zend_Date automatically corrects invalid dates. For this example, if the inputted date is invalid, I'd like the form to show again with an error message and the value that the user selected.
Validation
At this point, we have a form that can render a three-part date field, but cannot process it. In order to process it, we're going to need to do two things. Firstly, we'll need create a date validator to determine if the date selected is valid (i.e. not 31 June). Secondly, we'll need to join date_day, date_month and date_year into one value, so that when getValues() is called on the form only 'date' is returned.
Let's start with the validator:
<?php
class App_Validate_Date extends Zend_Validate_Abstract
{
const INVALID_DATE = 'invalidDate';
protected $_messages = array(
self::INVALID_DATE => 'Invalid date.'
);
public function isValid ($value, $context = null)
{
if (date('Y-m-d', strtotime($value)) != $value)
{
return false;
}
return true;
}
}
This is a very simple validator which uses the date() and strtotime() functions to determine if a given date is invalid. That's all very well, but we need to tell the date field to use this validator. So let's edit our element class:
class App_Form_Element_Date extends Zend_Form_Element_Xhtml
{
public $helper = 'formDate';
public function init ()
{
$this->addValidator(new App_Validate_Date());
}
public function isValid ($value, $context = null)
{
// ignoring value -- it'll be empty
$name = $this->getName();
$value = $context[$name . '_year'] . '-' .
$context[$name . '_month'] . '-' .
$context[$name . '_day'];
$this->_value = $value;
return parent::isValid($value, $context);
}
}
The init() function tells the element to add the date validator to the validation chain. The isValid() function is called to determine if a given value is valid against that chain. There's a little bit of trickery here: $value will always be NULL because there is no field in the form called 'date', but, thankfully, Zend_Form passes all $_POST values in as $context, so we can just grab _day, _month and _year to compile our date value. By setting $this->_value, getValues() will return the correct value as in our use case.
The nice thing about doing complex elements this way, is that the validator is not bound to the element (although in this case the reverse is true). Because validation logic has been encapsulated in a validator class, you could use the validator to validate dates in other contexts.
Although this example is very simple, it should give you the tools to create more complex compound elements. The theory of operation of element, view helper and validator applies to elements of any complexity.
Discussion
Subscribe to an RSS feed of these commentsfte
Jun 26, 2008 10:22:40 AM
Thank you very much for your article. I have issue to pass parameters at the creation of the object :$attribs = array('class', 'terrible');
$date = new My_Form_Date('date', null, $attribs);
but the attribute didnt show in the html rendered SELECT.
What's the best advantage to use Compound elements comparing to Zend_Form_Subform ?
Neil Garb
Jun 26, 2008 4:04:24 PM
@fte: To set the class you would instantiate the element as follows:$date = new My_Form_Date('date', array(
'attribs' => array(
'class' => 'test'
)
)
);
There are two advantages to using compound elements. Firstly, they better encapsulate a single field on a form. In this instance, the date is one value -- the fact that I've chosen to implement it using three select boxes is my choice, but it is still one value -- and should therefore be treated as a unit by Zend_Form. Secondly, using this technique allows you to create compound elements which don't necessarily use any HTML form elements. For instance, you could create a Flash app which and use a Javascript bridge to pass the value back to the form, yet it would be treated just like any other Zend_Form element.
Ygor
Jul 17, 2008 9:06:36 AM
Nicely done, however if you need to have a 'true' compound element characterized by having multiple values, you might consider doing the following. As an example take a survey 'other' option which often looks like [radio] Other: [text input]By adding the text input field to the form (or subform) with empty decorators, the form element is added like any other, but not rendered. Then by writing your own custom viewHelper and CompoundDecorator you can render the text input in the option of the radion element.
I hope this makes sense!
Cheers!
Ygor
Neil Garb
Jul 17, 2008 4:58:33 PM
@Ygor: thanks for your comments. I like your example, but I'm not sure it works in generality. My post was more about the logic behind adding arbitrary inputs (which might not necessarily be HTML form elements -- they might be Flash apps or activeX objects, provided they can bridge with the DOM somehow), and how to treat them homogeneously in ZF.Steve
Jul 18, 2008 10:06:34 AM
Great post I've been wondering about this for a while. I implemented the subform method previously, but could see that it was an ugly hack and didnt like it at all.I knew this was possible but the documentation is a little thin on the ground here for a newbie like me! You've plugged the gap, thanks.
Cthulhu
Jul 29, 2008 12:17:56 PM
Very nice, I've been looking for this one. Thanks loads, saves the trouble of having to implement something like this myself (which, due to my lack of experience with ZF would take several days). You may want to add or change the getValue() method to return a more independent date formatting, such as PHP's default time() format, instead of the YYYY-MM-DD string formatting. A call to strtotime() is made within seconds in the client code though, so it's all up to you.Vladas
Jul 29, 2008 2:11:33 PM
nice example!but one tiny thing IMHO in this example is not okay. Its that you are creating view helper in zend folder :)
So Zend_View_Helper_FormDate should be called App_View_Helper_FormDate
Neil Garb
Jul 29, 2008 2:11:50 PM
@Cthulhu: for sure -- I think there are a number of ways the handling of the date value could be improved, but my post was more about how to handle compound elements within ZF than about date formatting.Neil Garb
Jul 29, 2008 3:52:51 PM
@Vladas: I've always named the helpers I've put in application/views/helpers with the prefix Zend_, simply because you don't have to call any addPrefix() functions, but in this example it's a little unclear that the view helper is custom code.Have changed in the post. Thank you.
Cthulhu
Jul 31, 2008 9:05:29 AM
I've made some updates to your script so that the internal representation is PHP's own time() format. I encountered a problem with the conversion from form data to a Zend_Db_Table_Row (the User object in my case), when I attempted to do $user->setFromArray($form->getValues());. Before setting the user data however, a manual conversion to PHP's own time() format had to be done, which wasn't possible in the above very short form.So I made some tweaks to your script, so that internally it always uses PHP's internal time format. The changes are very small, only a few lines changed. I put it on here:
http://paste2.org/p/53824
Neil Garb
Jul 31, 2008 12:27:01 PM
@Cthulhu: thanks!Richard
Aug 6, 2008 9:50:38 AM
I'm being really silly here, but I keep getting the error:'Warning: helper 'FormDate' not found in path'.
I've tried what I think is correct in my bootstrap:
$view->addHelperPath(
self::$root . '/lib/App/View/Helper/'
);
but it's still complaining. Any suggestions appreciated!
Neil Garb
Aug 6, 2008 11:14:54 AM
@Richard: You should add your class prefix as the second parameter to addHelperPath():$view->addHelperPath('path', 'App_View_Helper');
Jaka Jančar
Aug 8, 2008 5:54:11 PM
Neil,thanks for the guide! I did it myself in pretty much the same way, except that I didn't think of just sticking the Date validator into the Element, which is a very nice addition :)
Now I'm faced with another problem and I've found this page while looking for a solution. I would love to hear your ideas about it, if you have any:
I need "array" inputs. When the user opens the web page, there could, for example, be 3 items (text-inputs or any other element) on a form. He could then click "+" to add another empty one, or click "-" next to any one of them to remove it.
I would of course do this "magic" using Javascript. The problem is integrating it so it works transparently using Zend_Form. AFAICS, Zend_Form's isArray() is primarily meant for subforms with strictly defined sub-elements, and not for "numbered arrays" (that is, rendering multiple xhtml inputs for a single element).
Ideally I would expect to pass in the existing values using $element->setValue(array('blah', 'bleh',...)) and retrieve them as an array using getValue() or $form->getValues().
The solution would have to work for all kinds of elements, both simple ones and compound ones.
Assuming I can do anything with Javascript, do you have any pointers on how to use it with Zend_Form?
Neil Garb
Aug 10, 2008 9:44:31 AM
@Jaka: the method that I outline here is perfectly suited to what you're trying to do. You can simply treat the set of input boxes as one compound element, create a view helper that wraps them in a div, and then do all the javascript unobtrusively. I would also create a class to represent the compound value of all those input boxes, and then write a validator for that class.I haven't had very much time to look at Matthew's dojo integration classes. There might be a shortcut somewhere in there, but this is how I would do it with what I know.
avik
Oct 30, 2008 11:27:53 AM
Warning: Plugin by name FormDate was not found in the registry. inhelp please, i always get an exeption. what i need to do, that correct.?
i place the classes into folder starting "ZF_"(instead Zend_), and folder structure is same like Zend framework.
include paths is made.
avik
Oct 31, 2008 1:08:43 AM
all works fine if a set view for all element seperately:$view = new Zend_View();
$view->addHelperPath('ZF/View/Helper', 'ZF_View_Helper');
$element->setView($view);
but if i want do that it gets automatically viewHelper path it dont work:
$form->setView($view);
it gives always error:
Warning: Plugin by name FormDate was not found in the registry.
what i do wrong?
axel
Nov 14, 2008 7:24:37 PM
avik: not sure if this is right, but I found the following worked for me,in my bootstrap I used:
Zend_Layout::startMvc($layoutConfig);
$layout = Zend_Layout::getMvcInstance();
$view = $layout->getView();
$view->addHelperPath(ROOT_DIR . '/application/models/MyApp/View/Helper', 'MyApp_View_Helper');
Pete Williams
Dec 10, 2008 10:53:37 PM
Great article, I haven't had time to go through the validation part yet but already it's helped me no end!One question I've got is regarding the elemend id's - when I view the source, all three elements have the id 'date', which means the page fails validation. Is there some way to specify a separate id for each element?
Julian Read
Dec 27, 2008 7:42:52 PM
@Pete Williams: When creating the drop down list using$this->view->formSelect(
$name . '_day',
$day,
$attribs,
$dayOptions);
Its the $attribs variable where extra attributes such as class and id can be added. So to change the id to month you can use the following
$this->view->formSelect(
$name . '_day',
$day,
array('id' => 'month'),
$dayOptions);
Obviously this is a quick fix and it would be better to find a solution by passing the values as parameters into the function.
Michael Smith
Jan 29, 2009 10:59:15 AM
Thanks for the interesting post. Seems to deal with one of the many areas where Zend Form is lacking. It's a shame none of this is detailed in the ZF Manual, as it's really useful.Neil Garb
Mar 26, 2009 12:35:54 PM
To everyone that found the post useful, I'm glad. I apologise for not responding quickly/individually. Unfortunately most of the interesting work I'm doing with ZF these days is owned by Zoopy, so I don't have too many interesting tidbits to share.Matt Parker
Mar 30, 2009 10:19:00 AM
Thanks for this, v helpful (and also Cthulhu).I've found a little bug in it, I think. It's been working fine until today (30/3/2009) , when the month select box had options 'January', 'March' , 'March' - Feb had disappeared.
It seems to be fixed by adding a zero to the Zend_Date:
$date = new Zend_Date( 0 );
to App_View_Helper_FormDate before the dayOptions etc are all built.
I think this is because the Zend_Date with no arguments sets the date as today, and so if the day of the month is (say) the 30th, when it comes to set the month as 2 (Feb) the full date becomes 30th February - i.e. 1st or 2nd March (depending on leap year).
Keith Miller
Aug 11, 2009 5:51:17 AM
Just wanted to say thanks for the post!Texas
Oct 8, 2009 9:56:56 AM
My form always give me this error Plugin by name 'FormDate' was not found in the registry; used paths: Zend_View_Helper_: Zend/View/Helper/;C:/xampp/htdocs/secure_shaadi_com/application/views\helpers/ inAny idea what i do wrong?
Your comment