Zend Form – Radio buttons

Post to Twitter

Zend Framework is very powerful and feature rich. But sometimes it can drive you nuts!
While working on one of my projects I needed to build a form looking something like this:

Looks pretty simple, but the trick is that I needed so that these were not 3 individual radio groups (Fruits, Veggies, Berries), but a single one. In other words, make it so that just one selection possible among all these radio buttons.

Difficulties

Zend Form allows us to create a group of radio buttons without a problem, so I started with something like this:

		$form->addElement('radio', 'radio_group', array(
			'label' => 'Options:',
			'multioptions' => array(
				1 => 'Banana',
				2 => 'Apple',
				3 => 'Pear',
				4 => 'Cucumber',
				5 => 'Tomato',
				6 => 'Potato',
				7 => 'Blackberries',
				8 => 'Raspberries',
				9 => 'Strawberries',
			),
			'required' => true,
		));

Ok, now how do we place some text(or whatever) in between so that to describe  groups? My next idea was to split that one big radio group into 3 smaller ones:

		$form->addElement('radio', 'fruits', array(
			'label' => 'Fruits:',
			'multioptions' => array(
				1 => 'Banana',
				2 => 'Apple',
				3 => 'Pear',
			),
		));

		$form->addElement('radio', 'veggies', array(
			'label' => 'Veggies:',
			'multioptions' => array(
				4 => 'Cucumber',
				5 => 'Tomato',
				6 => 'Potato',
			),
		));

		$form->addElement('radio', 'berries', array(
			'label' => 'Berries:',
			'multioptions' => array(
				7 => 'Blackberries',
				8 => 'Raspberries',
				9 => 'Strawberries',
			),
			'required' => true,
		));

Ok, that looks better, but now we have 3 separate groups, rather than one and you can have 3 selections and that’s not what we wanted to archive.

The problem here is that Zend Form elements are generated in such a way so that it puts name of the element into name of the HTML input control. So we have something like this (simplified):


<input name="fruits" type="radio" value="1" /> Banana
<input name="fruits" type="radio" value="2" /> Apple
<input name="fruits" type="radio" value="3" /> Pear

<input name="veggies" type="radio" value="4" /> Cucumber
<input name="veggies" type="radio" value="5" /> Tomato
<input name="veggies" type="radio" value="6" /> Potato

<input name="berries" type="radio" value="7" /> Blackberries
<input name="berries" type="radio" value="8" /> Raspberries
<input name="berries" type="radio" value="9" /> Strawberries

From the other hand, in order to make a radio group with HTML, you need to have the same name for all radio buttons.

With Zend Form it is impossible to just rename all 3 elements to have one name, as each element is required to have an unique name. Specifying name as an additional attibute for the control works to some extent, but it breaks other things like validation, repopulating radio group and others.

Solution

Ok, so I could give up and make everything with ViewScript writting pure HTML, but thinking about it I realized that this kind of task is quite frequent (especially when dealing with some kind of quiz or stuff like that). And I did not want to finish up having all my forms being decoupled from Zend Forms in favor of pure HTML. So I decided to make a custom form element to solve that, hopefully it will be useful for you also.

Let me first describe how we can use it, and later I will describe details of implementation if you’re interested:

  • First of all, we just create our “core” radio group containing all possible options:
		$form->addElement('radio', 'radio_group', array(
			'label' => 'Options:',
			'multioptions' => array(
				1 => 'Banana',
				2 => 'Apple',
				3 => 'Pear',
				4 => 'Cucumber',
				5 => 'Tomato',
				6 => 'Potato',
				7 => 'Blackberries',
				8 => 'Raspberries',
				9 => 'Strawberries',
			),
			'required' => true,
		));

This radio group will be used as a starting point for the other smaller groups. Why is it usable to have it, is that we can use validation configuration in just this one place, also by placing this element in different places we can control where exactly we want validation errors to be shown. And, when receiving value after form was validated, we don’t want to go and check at multiple locations which radiobuttom has which value, we can just check our “core radio button” for it.

  • We don’t want this element to output any extra markup, so lets leave only “Errors” decorator for it:
		$radio_parent = $form->getElement('radio_group');
		$radio_parent->setDecorators(array('Errors'));
  • We add 3 more radio groups for each set (Fruits, Veggies, Berries).

This time we’ll use extended version of Zend_Form_Element_Radio class which I’ve created, called “standaloneRadio”, like this:

		$form->addPrefixPath('ZFExt_Form_Element', 'ZFExt/Form/Element/', 'Element');

		$form->addElement('standaloneRadio', 'fruits', array(
			'bind' => array($radio_parent, array(1,2,3)),
			'label' => 'Fruits',
		));

It has an extra “bind” parameter which is an array:
- for the first item in this array you must use instance our “core radio group” element, created at step 1.
- for the second you specify which options from it you want to use.

So at the example above we refer to our core element ($radio_parent) and use options with IDs 1,2 and 3 from it, i.e. Banana, Apple, Pear. We’ll need to do the same thing for our “veggies” and “berries” elements, refering to corresponding options:

		$form->addElement('standaloneRadio', 'veggies', array(
			'bind' => array($radio_parent, array(4,5,6)),
			'label' => 'Veggies',
		));

		$form->addElement('standaloneRadio', 'berries', array(
			'bind' => array($radio_parent, array(7,8,9)),
			'label' => 'Berries',
		));

Basically that’s it! Now, when you validate the form and get the values, you will only need to refer to out ‘radio_group’ element and not individual elements:

		$form->radio_group->getValue();

Same thing applies when you want to populate form with default values – just assign it to the “radio_group” element and it will work as expected.

Implementation Details

So now let’s have a look at that custom class I’ve created:

<?php
class ZFExt_Form_Element_StandaloneRadio extends Zend_Form_Element_Radio
{
	//Parent element on which this radiobutton depends
	protected $_bindParent = null;

	//All validation is done at parent ($_bindParent), so we disable it for individual radiobuttons
	protected $_registerInArrayValidator = false;

	//This element depends on parent ($_bindParent), so we don't need its value directly
	protected $_ignore = true;

	public function setOptions(array $options)
	{
		//Accept additional bind parameter, which is an array:
		//0 - link to the parent radiobutton element (must instance of Zend_Form_Element_Radio)
		//1 - array of option ids which current element needs to take from its parent
		if (isset($options['bind'][0]) && $options['bind'][0] instanceof Zend_Form_Element_Radio) {
			$parent = $options['bind'][0];
			$binding_multioptions = isset($options['bind'][1])?$options['bind'][1]:null;

			if (!empty($binding_multioptions)) {
				if (!is_array($binding_multioptions)) {
					$binding_multioptions = array($binding_multioptions);
				}

				//Attach selected parent multioptions to the current element
				$parent_multioptions = $parent->getMultiOptions();
				$multioptions = array();
				foreach ($binding_multioptions as $current_option_id) {
					if (isset($parent_multioptions[$current_option_id])) {
						$multioptions[$current_option_id] = $parent_multioptions[$current_option_id];
					}
				}
				$this->addMultiOptions($multioptions);
			}

			$this->_bindParent = $parent;

			//We've stored all neccessary info, no need in this parameter anymore
			unset($options['bind']);
		}

		//Call original setOptions from Zend_Form_Element_Radio
		parent::setOptions($options);

		return $this;
	}

	public function render(Zend_View_Interface $view = null)
	{
		//Here we replace element name with parent name, but just for rendering
		$current_name = $this->getName();
		$this->setName($this->_bindParent->getName());

		$output = parent::render($view);

		//Put the real name back
		$this->setName($current_name);

		return $output;
	}

	public function getValue()
	{
		//Element is dependent on parent
		return $this->_bindParent->getValue();
	}

	public function setValue($value)
	{
		//Element is dependent on parent
		return $this->_bindParent->setValue($value);
	}

	public function isValid($value, $context = null)
	{
		//Assign same value to all members of the group
		$value = $this->_bindParent->getValue();
		$context[$this->_name] = $value;

		return parent::isValid($value, $context);
	}
}

This class is inherited from Zend_Form_Element_Radio as we want to take all features from it and just extend a little.
There’s not too much code and I commented it alot, so hopefully it will be easy to understand. But still, I will explain few points:

First of all, it overrides setOptions() function so that to read that extra “bind” parameter for our singular radio buttons.

It also overrides render() function. There I simply replace name of the individual radio group with the name of the “core element”. That way we ensure that all radio buttons in group will have the same name. Zend might need to use element name in many different places, so I’ve decided to put the original name back right after rendering.

Few more overrides for getValue() and setValue() functions. As you can see, they are now configured to get/set values directly from the core element. That way we ensure values are repopulated correctly on the form.

Last, and very importaint override is for isValid(). When data comes from HTML form, it only has value for “core element”. During validation we assign that same value for all linked radio buttons.

Alright, so that’s it! I hope that was useful. You can download entire code for this post here.

And if you guys have any questions or comments I’d be glad to hear ;)

Post to Twitter

5 Comments

  1. Alex says:

    Hello, Pavel.
    You should use formRadio helper for this case with this kind of params:
    $this->formRadio(‘allsites’, $form->getElement(‘sites’)->getValue(), null, array(’0′=>’All sites’))

    where array(’0′=>’All sites’) is value and label for radiobutton, etc.

  2. Bernard says:

    Hi Pavel,

    Thanks a ton for your post! It is very helpful.

    I am new to Zend and trying to write a form with several radio elements but have some problems. Would you look at the post of my question on http://stackoverflow.com/questions/15302917/how-to-add-radio-element-inside-another-radio-element-in-zend-framework. Thanks in advance.

  3. Hi Bernard, if you read carefully this post, I am describing almost exactly your situation here. By making combination of radio groups and standalone radios you can reach this effect.

Leave a comment