Oct 1

I’ve started using Zend Framework for a project I’m under taking here at TradeDoubler. I’m building a new part of Searchware that is essentially standalone, so figured that this is a great oportunity to push for framework support. Better form validation, less scope for creating errors in trivial donkey work coding because it’s already done for you, and ultimately a better experience for the user.

The problem I soon discovered with ZF, is that the documentation is not as good as I would of hoped. Their introductory videos are absolutely amazing, but when it comes to getting a real project started, they leave you feeling a bit left out in the cold.

Multi Page Forms

A good example of this, and something I’ve just been working on, is multi page forms. Zend Form is a great start and will go places, but right now, I think it’s not quite there. I discovered that subforms are the recommended way to implement multi page forms, but the example in the documentation again doesn’t quite explain how to do it, it just points you in a direction and expects you to figure the rest out for yourself. All very good, but some of us are fairly busy and would rather just read a comprehensive example.

A comprehensive example :)

This is how I decided to make a multi page form based on Zend Form subforms. I don’t know if this is the best way of doing it, and I am a complete newbie to ZF, but since I couldn’t find any other examples, and this does work, I’ll just have to presume it is until I’m corrected by one of you kind readers :p. This example will show you how to setup the required classes, build a simple form, validate, and then store the information and make it available to subsequent forms for decision making.

Note: This is not a beginners guide to Zend Framework or MVC. If you’re not quite sure how ZF works, or what MVC is, please check out the introductory vids on the Zend site. They are very good.

So, to kick things off we’re going to need to load up all the classes required for our forms. To do this, add the following lines to your boot strap file.


DEFINE('APPLICATION_PATH','/data/web/yourApplication');

Zend_Loader::loadClass("Zend_Form");
Zend_Loader::loadClass("Zend_Session");
Zend_Loader::loadClass("Zend_Session_Namespace");

// And any validation classes you will be using, for example
Zend_Loader::loadClass("Zend_Validate_NotEmpty");

This will set up our bootstrap file with everything we need to build a form, so the next job is editing your controller class. Add the following methods into your controller. They are used to store and read validated form values, but more on that later.


	private function storeFormValues(Zend_Form $form)
	{
		$formSession = new Zend_Session_Namespace('yourAppForm');

		foreach ($form->getValues() as $key => $value)
		{
			$formSession->$key = $value;
		}
	}

	private function getFormValues()
	{
		$formSession = new Zend_Session_Namespace('yourAppForm');

		$data = array();
		foreach ($formSession->getIterator() as $key => $value)
		{
			$data[$key] = $value;
		}
		return $data;
	}

You will also need to add the following method in your controller class.


	 protected function getForm($formName)
	 {
	 	// you will need to edit this later, but leave it for now.
	 	require_once APPLICATION_PATH . '/forms/parentForm.php';

	 	$mainForm = new Form_ParentForm($this->getFormValues());

	 	if ($formName == 'main')
	 	{
	 		$form = $mainForm;
	 	}
	 	else
	 	{
	 		$form = $mainForm->getSubForm($formName);
	 		$form->addElement('hidden','currentFormStage',array('value' => $formName));
	 	}

	 	return $form;
	 }

So, I’ll take a little time to explain that one since it’s not instantly obvious.

First off

require_once APPLICATION_PATH . '/forms/parentForm.php';

is the path to your form classes. I’ll explain how to create those later but for now, decide where you will want to store your forms, and point this there. Remember the constant APPLICATION_PATH was set in the bootstrap file.

The next line is

$mainForm = new Form_AddAccount($this->getFormValues());

This instantiates our parent form and passes to it any form data we have in our session.

The next part is

if ($formName == 'main')
{
	$form = $mainForm;
}

This is used later on in the controller to check whether the entire form (i.e. all of it’s sub pages are validated). The controller asks for the sub form name, but if this is ‘main’, then the parent class is sent back.

Creating our forms
So far we’ve built the required scaffolding for our multi page form that will be used by the controller. The next step is to create the forms themselves. As I’ve already mentioned the overall multi page form consists of a parent container form, and a collection of sub forms. For the sake of making it easy to read, I’m going to use VERY crude examples of forms, but please consult the Zend Form docs for more details about creating various form elements. That part of things is fairly well documented.

The entire parent class looks like this…


class Form_ParentForm extends Zend_Form
{
	private $formValues;

	public function __construct($formValues)
	{
		$this->formValues = $formValues;
		parent::__construct();
	}

	public function getFormValue($name)
	{

		if (isset($this->formValues[$name]))
		{
			return $this->formValues[$name];
		}
		else
		{
			return null;
		}

	}

	public function init()
	{

		$this->setAction('index');
		$this->setMethod('post');

		require_once APPLICATION_PATH . '/forms/SubFormPageOne.php';
		require_once APPLICATION_PATH . '/forms/SubFormPageTwo.php';

		$pageOne = new Form_SubFormPageOne($this);
		$pageTwo = new Form_SubFormPageTwo($this);

		$this->addSubForm($pageOne,'pageOne');
		$this->addSubForm($pageTwo,'pageTwo');

	}

}

The only bit you need to be concerned about editing here is the init() method. Change setAction() and setMethod() as you see fit, but they will probably be ok as they are in most cases.
The next bit, the requires, is important. Remember in the controller class we edited the getForm() method. There was an include path in there that pointed to the parent form. You need to make sure that, obviously, this parent form is saved to the same place. You could put the subforms in other directories, but I don’t see any benefit of doing so, so I’d recommend you keep then all bundled together in the same directory.

Once you have included then, you instantiate the subforms (actually, they are instances of Zend_Form and not SubForm, but that is ok), and then pass them to addSubForm. Hopefully this is quite easy to follow so I’m not going to explain it any further. If you get stuck, please feel free to ask me a question.

So then, our final step in building the forms is to create the sub forms. The subform class looks like this.


class Form_SubFormPageOne extends Zend_Form
{
	private $parentForm;

	public function __construct(Zend_Form $parentForm)
	{
		$this->parentForm = $parentForm;
		parent::__construct();
	}

	public function init()
	{

		// engine dropdown
		$engineSelect = $this->createElement('select','engine');

		$engineSelect->addMultiOption('','Please Choose...');
		$engineSelect->addMultiOption('google','Google');
		$engineSelect->addMultiOption('yahoo','Yahoo');
		$engineSelect->addMultiOption('msn','MSN');

		$engineSelect->setRequired(true);

		$this->addElement($engineSelect);

		// create submit button
		$this->addElement('submit', 'btnNext', array( 'label' => 'Next')); 

	}

}

Apart from changing the class name to suit your needs, the only other thing you should need to edit is the init() method. In here you create the form elements, apply validation, decorators and so on. This work in exactly the same way as a single page form, so please consult one of the many Zend Form examples for details on adding elements. As you can see our example form simply gives a dropdown list of search engines and a submit button.

Plugging it all together
So we’ve got our scaffolding, and we’ve got our forms. The only thing left to do now is to stick it all together, and this happens in the ‘action’ method of the controller. In most cases, and certainly this one, it will be the index action.

This method is a bit longer than the others so rather than me blabbering on here, I’ll let the comments to the talking. :)


public function indexAction()
{
	$request = $this->getRequest();

	// is this a post back, i.e was the form submitted or is it a first visit.
	if ($request->isPost())
	{
		/*
		Get an instance of the current form.
		Remember currentFormStage was appended
		to the form as a hidden field in the getForm method.
		*/
		$form = $this->getForm($_POST['currentFormStage']);

		// does is pass validation?
		if ($form->isValid($_POST))
		{
			// yes, so save the values to our session.
			$this->storeFormValues($form);

			/*
			So, we've just check a subform and it was valid.
			Does this now make our entire form collection valid?
			Let's check by getting in instance of the parent form.
			*/
			if ($this->getForm('main')->isValid($this->getFormValues()))
			{
				/*
				The form is complete, so redirect to the
				finish action (you will need to create this)
				*/
				$this->_redirect("index/finish");
			}

			/*
			A crude but workable method of choosing which form to go to next.
			*/
			switch ($_POST['currentFormStage'])
			{
				case 'pageOne':
					$newForm = 'pageTwo';
					break;
				default:
					$newForm = 'pageOne';
				break;
			}
			/* get an instance of our new form.
			having passed page one, this would be now page two.
			*/
			$form = $this->getForm($newForm);

		}

	}
	else
	{
		/*
		If this is the first time the page is loaded
		i.e. no forms submitted, let's make sure the session is
		empty.
		*/
		$formSession = new Zend_Session_Namespace('yourAppForm');
		$formSession->unsetAll();

		// and then load the first form page.
		$form = $this->getForm('pageOne');
	}

	$this->view->printForm = $form;

}

And there it is, you’re done. You have a working multi page form in the Zend framework. One final note, the last line $this->view->printForm = $form; is simply to pass the form to the view. The view file for this controller/action, would contain printForm; ?>

I hope that helped clear things up, and if anybody has any questions, please feel free to post them.


7 comments so far...

  • nick Said on June 30th, 2009 at 8:43 am:

    thank you very much for this post. It seems incredible that Zend offer such a great framework but then neglect to include support for something as crucial as multi-page forms. I found your tutorial a huge help and easy to follow, thanks again!

  • Mirko Said on July 25th, 2009 at 4:28 pm:

    It works very good except one situation, I can`t figure out how to use it with Zend_File? This condition “if ($this->getForm(’main’)->isValid($this->getFormValues()))” was always return false.

    Here is a part of onces of my subform:
    $picture=new Zend_Form_Element_File(’picture’);
    $picture->setLabel(’Chose picture:’)
    ->setRequired(false)
    ->setDestination(’pictures’);

    $submit=new Zend_Form_Element_Submit(’Save’);
    $this->addElements(array($picture,$submit));

    File is uploaded to right place, but last validation returns false.
    I will be really appriciate for replay.

  • Mirko Said on July 25th, 2009 at 6:38 pm:

    The answer is $picture->setValueDisabled(true) and after validation just check:
    $adapter=new Zend_File_Transfer_Adapter_Http();
    $adapter->setDestination(’pictures’);
    if ($adapter->receive()) {
    echo ’success’;
    }
    It takes me almost whole day :-)

  • Patrick Said on August 25th, 2009 at 12:11 am:

    Your code seem to work, but I cannot get the second form to display. The master form is always valid, so it doesn’t show the second page. Also, I would like to include the “back” button on the second and the finish (preview) form and when users press on the “back” button the previous form will re-display with entered data.

    Any assistance would be appreciated.
    Thks

  • jack Said on September 23rd, 2009 at 11:18 pm:

    i can’t get this work somehow.. i got everything the same, what am i missing? patrick can u upload a zip version or something?

  • Restaurant Webdesign Said on January 12th, 2010 at 12:00 am:

    Thanks for the tutorial. Unfortunately I can’t get it to work either. The first page of the form is shown but when I click the next button it goes straight to the ‘finish’ page.

  • Restaurant Webdesign Said on January 12th, 2010 at 12:07 am:

    I discovered that I had just made a silly mistake!
    I copied the code from the first sub form page but didn’t rename the element in the second subform.
    As soon as I changed the element in the second subform to $engineSelect2 the form worked as expected.

leave a reply