Form Handling With the Zend_Form Proposal Implementation
We're all acquainted with the tedious, repetitive task of form handling. Hoping for a relieve from the form-madness I was a bit disappointed Zend Framework 1.0 didn't come with a Zend_Form component. There was a proposal for it in the ZF wiki, but it wasn't updated for a while. Seeing as waiting and complaining is never a good solution, I decided to write my own Zend_Form implementation and proposal. One of the primary design goals was to make the Zend_Form fit in with the ZF MVC implementation. At the same time the Zend_Form needed to be as flexible as possible. For inspiration on how to accomplish this, I looked at the available proposal and it's comments and followed various discussions about a Zend_Form component on #zftalk (irc.freenode.net).
For the presentation of the form I chose to make it possible to use the view helpers that came with ZF 1.0. But, to make life easier for the lazy developers I also made a few view helpers of my own. These new helpers make use of the ability to specify type hints in a form element. With the type hints it is possible to separate the view completely from the actual form models, while keeping the luxury of automating form building.
The first helper reads the type hint from the form element and renders the corresponding stock view helper. The second takes this process one step further. It automatically generates a complete, working form. While this doesn't provide the flexibility needed in a production environment, it's ideal for quickly developing and debugging your applications, while the art-team is still drawing up a nice GUI.
To prevent expired forms it is a good thing to issue a redirect after the submitted form data has been processed. In order to keep the form data a storage mechanism needed to be developed. An obvious way to store data across requests is by using sessions. An alternative method that is currently supported in Zend_Form is using the Zend_Cache to store the data. The various form pages are serialized to storage as a whole. This way the form only needs to be setup once.
An important feature of a form component is validation and filtering of the incoming data. Zend_Framework already has support for validation and filtering with the Zend_Validate and Zend_Filter components. Each form element in Zend_Form can contain one or more validators or filters (or chains for that matter).
Let me give you a concrete and practical example. I supplied comments in the code to explain the working. In this example we want to add or edit a car. A car can have a color, which can be either red, green or blue, and it can have a number of doors, which needs to be an integer. To identify the car in the database we use the primary key, which again is an integer.
public function init()
{
/**
* In this example the gateway is a Zend_Db_Table_Abstract subclass and returns Car objects on return.
* The Car objects are subclasses of Zend_Db_Table_Row_Abstract.
*/
$this->_gateway = new Car_Gateway();
/**
* Instantiate the special FormRedirector helper. Basicly this is the normal redirector with two extra methods.
*/
$this->_redirector = $this->getHelper('FormRedirector');
}
public function editAction()
{
/**
* Fetch a form page instance. If it's available in storage, retrieve it. Otherwise make a new instance.
* The default data source is $_POST
*/
$form = Zend_Form_Factory::get($this->_formName, $this->_getParam('id'));
if (!$form->fromStorage()) {
/**
* The page wasn't available in storage, so we're setting up a new one
*/
/**
* Colors our car can have.
*/
$colors = array(
'ff0000' => 'Red',
'00ff00' => 'Green',
'0000ff' => 'Blue'
);
/**
* ID Element:
* When using the formFied helper, it will output
* <input type="hidden" name="id" value="" />
*/
$form->addElement('id')
->addValidator(new Zend_Validate_Int()) // The ID needs to be an integer
->setAllowEmpty(true) // It can be empty, in case we want to add a new car to our DB
->addTypeHint(Zend_Form_Element_Hint_HTML::FORMAT, Zend_Form_Element_Hint_HTML::HIDDEN); //The type hint. The hint implies that if the field is outputted in HTML, it should be an
/**
* Color field:
* When using the formElement helper, it will output
* <select name="color">
* <option value="ff0000">Red</option>
* <option value="00ff00">Green</option>
* <option value="0000ff">Blue</option>
* </select>
*/
$form->addElement('color', 'Zend_Form_Element_Set') //Specify the field class. It needs to be able to contain the options.
->addValidator(new Zend_Validate_InArray(array_flip($colors))) // The only valid values are those in the $colors array
->setOptions($colors) //Add the options to the set
->addTypeHint(Zend_Form_Element_Hint_HTML::FORMAT, Zend_Form_Element_Hint_HTML::SELECT); //And the type hint
/** Doors Element */
$form->addElement('doors')
->addValidator(new Zend_Validate_Int())
->addValidator(new Zend_Validate_GreaterThan(1))
->addTypeHint(Zend_Form_Element_Hint_HTML::FORMAT, Zend_Form_Element_Hint_HTML::TEXT);
/** Engine Element */
$form->addElement('engine')
->addValidator(new Zend_Validate_Alnum())
->addTypeHint(Zend_Form_Element_Hint_HTML::FORMAT, Zend_Form_Element_Hint_HTML::TEXT);
/**
* Submit buttons.
* All submit button names should be prefixed with a double underscore. This way, the submit action can be determined easiliy.
* For your comofort, there are a few constants available for common submit buttons.
*/
$form->addElement(Zend_Form::SUBMIT_OK)
->setValue('OK'); //The text on the button
$form->addElement(Zend_Form::SUBMIT_RESET)
->setValue('Reset');
$form->addElement(Zend_Form::SUBMIT_CANCEL)
->setValue('Cancel');
/**
* If we're editing a entry from the DB, we want to load it once and set the default values for the form fields to the values in the entry.
*/
if ($this->_getParam('id') != null) {
/**
* Fetch object from db
*/
$car = $this->_gateway->getById($this->_getParam('id'));
/**
* And assign it's values to the form fields.
*/
$form->setDefaultData($car->toArray());
}
}
/**
* Process the form page. If there hasn't been any submission process() will return false.
* Otherwise all fields will receive their value which will be filtered and validated.
*/
if ($form->process()) {
/**
* We want to know which button was pressed when submitting the form.
* The process action was able to provide this information by using the double underscore prefix on submit button names.
*/
switch ($form->getSubmitAction()) {
/**
* OK Button was pressed.
*/
case Zend_Form::SUBMIT_OK:
/**
* If all data is valid, forward to the save action to persist the data.
* Otherwise, redirect to re-display the form.
*/
if ($form->isValid()) {
$this->_redirector->formForward($form, 'save');
} else {
$this->_redirector->formRedirect($form);
}
break;
case Zend_Form::SUBMIT_RESET:
$this->_redirector->formForward($form, 'reset');
break;
case Zend_Form::SUBMIT_CANCEL:
$this->_redirector->formForward($form, 'cancel');
break;
}
} else {
/**
* There was no submission, so assign the form page to the view so it can be rendered.
*/
$this->view->form = $form;
}
}
public function saveAction()
{
/**
* Fetch the form from storage
*/
$form = Zend_Form_Factory::get($this->_formName, $this->_getParam(Zend_Form::FORM_IDENTIFIER_FIELD));
/**
* If the data isn't valid yet, go somewhere else
*/
if (!$form->isValid()) {
$this->_redirector->goto('index');
}
$id = (int)$form->id->value;
/**
* Collect the data
*/
$data = array(
'color' => $form->color->value,
'doors' => $form->doors->value,
'engine' => $form->engine->value
);
/**
* Persist the data
*/
if ($id > 0) {
$car = $this->_gateway->find($id)->current();
} else {
$car = $this->_gateway->createRow();
}
$car->setFromArray($data);
$car->save();
/*
* Remove the form page from storage, we don't need it anymore
*/
Zend_Form_Factory::remove($this->_formName, $this->_getParam(Zend_Form::FORM_IDENTIFIER_FIELD));
/*
* Redirect to the view page to have a look at our fresh entry.
*/
$this->_redirector->goto('view', null, null, array('id' => $car->id));
}
In the view, the form can be displayed, as described earlier, by using view helpers.
// The lazy man's way: generate a complete form
echo $this->makeForm($this->form);
// Or for more flexibility, use the formField helper
echo $this->formField($this->form->id);
echo $this->formField($this->form->color);
echo $this->formField($this->form->doors);
This was a quick overview on how to use the Zend_Form proposal implementation. For more detailed information on how to use the Zend_Form component, check out the documentation PDF and the implementation itself at my SVN repository. The source code in this article is also available as sample application in the same repository.
Please note that this is still a proposal and not a core component of the Zend Framework. The API might change at's even possible it will never end up in the Zend Framework core. Please do provide comments on the implementation and proposal, so the chances of successful acceptation increase.

Comments
public function editAction()
{ ... etc ...
Your implementation of this class method represents nothing more than putting a procedural function inside a class. There is a lot you could do to improve the design as it stands at the moment.
Fortunately object oriented development today says that we as developers need to do more than merely put procedural functions wrapped in a class definition. How you add the variable elements that constitute what a form is should be encapsulated in their own [I]Form Policy[/I] for example.
There are other issues... How you process the form should be encapsulated into their own classes also, so you gain the ability to maximise reuse of the class hierarchy. Means that it's easier to test as well at the end of the day.
Sorry for being so negative, but this implementation is what I see a lot of in the Zend Framework, and it's a mess to be honest with you. I was actually expecting,... No wait!! I [B]demand[/B] a better designed and implemented framework, than what we currently have today.
Sadly lacking I'm afraid :(
Please note that the good people here at Devzone are busy with a lot of things and take a while to process articles that are sent in by people like me. Therefor the article lags behind on the rapid development of the form proposal and implementation.
Having said that, I think it's OK for you to be negative about my Form component, or even ZF in general.
But, instead of just randomly demanding something, please try to be concrete in your wishes and specify areas where you think improvement is needed. It would be even better if you came up with solutions to the problems you see. Help is always apreciated.
Aside from being surprised that devzone haven't asked you to change it for being offensive, your username does lead a reader to expect you to be trolling whether you are or not.
I have to agree with norm2782 and also suggest that give something back instead of just "demanding" which makes you sound a bit like a spoilt child.
Having said that it's hard to make any comment as there are currently several Zend Framework form proposals and as norm2782 says the time lag between writing and publishing means this is probably a bit behind the overall progress. The best suggestion would be to view this in context of the other form proposals on the Zend Framework Wiki.
Norm2782, this is looking pretty good. I imagine alot of the design ideas came from that long discussion in #zftalk 3-4 months ago, and have to say this is definitely a good step in the right direction for the (late-to-the-party) component ;)
The controller should simply receive the actions (once submitted), be given access to the validation data (after first-stage validation has been completed) and then decide, based upon this, what the response should be.
The view/template should setup the form, setup and style the elements and define the helpers to use to do first-stage validation of input (and allow reflection of invalid responses).
Why do you create a form in the controller, and then add form elements there as well when have a perfectly valid view/template to do this? The controller should be abstracted from the form and should be responsible for receiving data, actions and decide what to do from that point on.
I look forward to a Zend_Form component but it has to be one of it's time; namely 'the web2.0 era'. Zend_Form should therefor definitely make handling AJAX'ed forms easier. I hope all wikis, thoughts and ideas will mash up to some sort of KEEP-IT-SIMPLE approach without losing focus on a flexible design in the view.
This here und that:
http://mitchellhashimoto.com/zend-form
Wouldn't it be better to merge the best oft this two Versions into one?
Or are they completely different.
For me it might be quite ok, however there are some concerns:
1. What about handling multi-page forms? Those so-called 'wizards' which are always pain-in-the-ass for developers because users may want to go forward and backward all the time.
This proposal:
http://framework.zend.com/wiki/display/ZFPROP/Zend_Form+-+Mitchell+Hashimoto
is concentraded on multi-page but does not have such simple and easy-to-use database support
2. What about AJAX support which you did not mentioned? e.g. saving temporary values for user like gmail do.
> your username does lead a reader to expect you to be trolling whether you
> are or not.
I don't understand what you mean by 'trolling'? Maybe you can explain what you mean by it. I've heard of the expression before on a number of forums but never understood why or where it comes from.
If my username has caused offence in any way then I'm sorry but that wasn't the intentions of picking the email address it's self, but more to do with my dark humour ;)
Of course no one is going to nuke Baghdad, nor any other capital city of a state or country. Or at least I hope not.
In answer to my earlier comments, what I do for form validation is to use the Specification Pattern, which separates validation rules into individual rules. Doing so has it's obvious benefits, however did you know that you could (therefore) use logical AND and OR to build a validating Policy?
That is what I was talking about. As for presentation of forms, I use PHP within the template it's self - nothing complicated. For SELECTs, RADIOs and CHECKBOXes I use another approach to separate the business logic from the template.
As for the Zend Framework, I did have expectations of it, but 12 months on I have a number of doubts. Nothing serious but just a few choice decisions that were made in regards to it's design.
The premise of the framework was to keep it lightweight, simply etc but that's isn't what I see today; It's rather bloated.
final class QBody extends QPage_Handler_Specification {
public function __construct() {
$this -> id = 'phptag:body';
$this -> initialise();
}
public function execute( QDataspace_Interface $dataspace ) {
if( $this -> validate( $request = QRegistry::get( 'request' ), QRegistry::get( 'logger' ) ) ) {
$this -> handler -> execute( $dataspace );
} else {
$page = new QPage_View( $request );
$page -> render( 'members/signin/body.tpl' );
}
}
protected function initialise() {
$this -> setHandler( new QSuccess() );
$this -> addSpecification( new QSpecification_Rule_Is_Postback() );
}
}
// You arrive here next, if all is well then continue, otherwise
// execute the next handler
final class QSuccess extends QPage_Handler_Specification {
public function __construct() {
$this -> id = 'phptag:body';
$this -> initialise();
}
public function execute( QDataspace_Interface $dataspace ) {
if( !$this -> validate( $request = QRegistry::get( 'request' ), QRegistry::get( 'logger' ) ) ) {
$this -> handler -> execute( $dataspace );
} else {
// ... process validated inputs
}
}
protected function initialise() {
$this -> setHandler( new QFailure() );
$this -> addSpecification( new QSpecification_Rule_Is_Required( 'username', 'Username is a required field' ) );
$this -> addSpecification( new QSpecification_Rule_Is_Required( 'password', 'Password is a required field' ) );
}
}
final class QFailure extends QPage_Handler {
public function __construct() {
$this -> id = 'phptag:body';
}
public function execute( QDataspace_Interface $dataspace ) {
$page = new QPage_View( $request = QRegistry::get( 'request' ) );
$page -> set( 'logger', QRegistry::get( 'logger' ) );
$page -> render( 'members/signin/error.tpl' );
}
}
// The above controller structure is based on the Chain of Responsibility pattern
// The Specification pattern is in each of those rules,
// Class QPage_Handler_Specification
// ...
protected function validate( QDataspace_Interface $request, $logger ) {
foreach( $this -> filters as $filter ) {
$filter -> process( $request );
}
$validation = true;
foreach( $this -> specifications as $specification ) {
$validation = $specification -> isSatisfiedBy( $request, $logger ) && $validation;
}
return $validation;
} // ... etc ...
See how the form validation is completely separate from the controller? Do you notice how easily I could alter the form validation rules themselves, without effecting other parts of the system?
This is why I made the initial comments that I did, because there is better ways in how to do it, and no you don't need to wrap procedural script wrapped in a class definition, and pass it off as OOP to do so either.
At the heart of object oriented methodologies, is design first and formost, and sadly design is lacking from the examples posted in this article. So many people get OOP wrong from the word go nowadays, and the examples given are one reason for it all going PEARshape ;)
But I don't want to rant either...
Good to see your comments are serious.
As I said, the article is a bit outdated. To get a better idea of the Form component I'm discussion, please have a look at the proposal: http://framework.zend.com/wiki/pages/viewpage.action?pageId=36061
In the proposal is a link to a SVN repository where you can find the latest implementation and example application.
Your code sample looks interesting (gonna have a better look at it tomorrow ;). The Specification Pattern is also definately something I'll look in to. Anyway... if you ever feel like a more direct discussion (goes for anyone really) about the design of this Zend_Form, please feel free to join #zftalk @ irc.freenode.net.
@Zoer Znu
I have yet to contact Mitchell, but as for your question: the official Zend_Form implementation will probably do just that; merge the best of the various form implementations into one.