Categories


Loading feed
Loading feed
Loading feed

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


Friday, September 28, 2007
PROCEDURAL JUNGLE
2:56PM PDT · nukebaghdad
Saturday, September 29, 2007
YEA
10:47PM PDT · Nozavroni
Monday, October 1, 2007
HARD PROBLEM
5:43PM PDT · miallen
Tuesday, October 2, 2007
RE: PROCEDURAL JUNGLE
11:28AM PDT · norm2782
RE: PROCEDURAL JUNGLE
9:40PM PDT · nicklo
Wednesday, October 3, 2007
MY COMMENTS & RE: PROCEDURAL JUNGLE
10:39AM PDT · snarff
Sunday, October 14, 2007
NOT A REALLY GOOD STRUCTURE
2:37PM PDT · Anonymous User [unregistered]
Tuesday, October 16, 2007
AJAX
12:40AM PDT · Sébastien Jacobs [unregistered]
Wednesday, October 17, 2007
2 VERSIONS OF ZEND FORM
12:33AM PDT · Zoer Znu [unregistered]
Saturday, October 20, 2007
MULTI-PAGE FORMS AND HANDLING UPLOADING FILES WITH AJAX PROGRESS-BAR
8:59AM PDT · chesteroni [unregistered]
Wednesday, October 24, 2007
IN REPLY TO MY EARLIER COMMENTS
1:14AM PDT · nukebaghdad
EXAMPLE OF SPECIFICATION PATTERN
1:27AM PDT · nukebaghdad
RE: COMMENTS
7:51AM PDT · norm2782
Loading feed