Another less-sysadmin-y, more code-y post today.

Zend_Form is pretty handy, and takes care of a lot of the hard work in producing and validating forms. Unfortunately the default decorators aren't quite as sane in my opinion, which becomes obvious if you start using fieldsets, or display groups, as ZF refers to them - You'll see your fieldsets getting wrapped in an additional definition list which is basically crap if you ask me. You can get rid of them with CSS, but its tricky.

To avoid this, until today I've been using a set of custom decorators and getting increasing frustrated in having to add more and more to support ZendX_JQuery_Element's and Zend_Form_Element_File, etc. Feeling somewhat defeated as I was hitting the limits of the decorators documentation, I started taking another look at the default decorators and what could be done with the minimal amount of tedious work, in terms of drop in replacements and slight CSS alteration. I came up with a reasonably good solution that I cannot believe I didn't see before.

I've plonked the code below (sanitized from anything specific to my setup - such as HTMLPurifier integration, etc. - with a test form to help you see how it works) incase anyone happens to be interested. Given the unusual increase in commenting I've been getting against using Zend Framework and the PHP Sqlsrv extension I figured it might be worth posting (I'm sure I'm not the only person out there who has missed the obvious).

What I've basically done is tell the form to render form elements, wrapped in a form. Display groups should then only display the elements, wrapped in a dl, wrapped in the fieldset. I've added some “plain” fieldsets which I use for control elements, such as submit or reset buttons. I've also given these items classes which allows me to remove the fieldset through styling, and move the positioning easily.

class My_Form extends Zend_Form
{
	// Form
	protected $_formDecorator = array('FormElements', 'Form');

	// Display Groups
	protected $_groupDecoratorStd = array('FormElements', array('HtmlTag', array('tag'=> 'dl')), 'Fieldset');
	protected $_groupDecoratorCtl = array('FormElements', 'Fieldset');

	// Ctls and hidden elements
	protected $_elementDecoratorCtl = array('ViewHelper');

	public function __construct($options = null)
	{
		parent::__construct($options);

		// Add our HTMLPurifier filter(s)
		$this->addElementPrefixPath('My_Filter', 'my/Filter/', 'filter');
		// Add our Confirmation validator
		$this->addElementPrefixPath('My_Validate', 'my/Validate/', 'validate');
		// Add our custom elements path
		$this->addPrefixPath('My_Form_Element', 'my/Form/Element/', Zend_Form::ELEMENT);

		$this->setAttrib('accept-charset', 'UTF-8');
		$this->setMethod('post');

		$this->setDecorators($this->_formDecorator);

		$this->setDisplayGroupDecorators($this->_groupDecoratorStd);
	}

	public function addAntiCSRF($salt, $timeout = 300, $name = 'no_csrf_foo')
	{
		$this->addElement('hash', $name, array(
			'decorators' => $this->_elementDecoratorCtl, 
			'salt' => $salt,
			'timeout' => $timeout
		));
	}

	public function addSubmit($labelSubmit = 'Submit')
	{
		$this->addElement('submit', 'submit', array(
			'label' => $labelSubmit,
			'decorators' => $this->_elementDecoratorCtl,
			'class' => 'submit'
		));

		$this->addDisplayGroup(
			array('submit'), $this->getGroupName('controls'),
			array(
				'legend' => 'Controls', 
				'class' => $this->getControlsClass(),
				'decorators' => $this->_groupDecoratorCtl
			)
		);
	}

	public function addSubmitReset($labelSubmit = 'Submit', $labelReset = 'Reset')
	{
		$this->addElement('submit', 'submit', array(
			'label' => $labelSubmit,
			'decorators' => $this->_elementDecoratorCtl,
			'class' => 'submit'
		));

		$this->addElement('reset', 'reset', array(
			'label' => $labelReset,
			'decorators' => $this->_elementDecoratorCtl,
			'class' => 'reset'
		));

		$this->addDisplayGroup(
			array('submit', 'reset'), $this->getGroupName('controls'),
			array(
				'legend' => 'Controls', 
				'class' => $this->getControlsClass(),
				'decorators' => $this->_groupDecoratorCtl
			)
		);
	}

	protected function getGroupName($name)
	{
		return get_class($this).'-'.$name;
	}

	protected function getControlsClass()
	{
		return array('controls', get_class($this).'-controls');
	}

	protected function getInputsClass()
	{
		return array('controls', get_class($this).'-inputs');
	}
}

class Test_Form extends My_Form
{
	public function init()
	{
		$this->addElement('text', 'title', array(
			'label' => 'Title',
		    'validators' => array(
		    	array('StringLength', false, array(2,50))
			),
		));
		
		$this->addElement('file', 'file', array(
			'label' => 'file',
		));
		
		$elem = new ZendX_JQuery_Form_Element_DatePicker(
			'dp',
			array(
				"label" => "Date Picker",
				'validators' => array('Date'),
				'required' => true
			)
		);
		
		$this->addElement($elem);
		
		$this->addElement(
			'hidden', 'shush', array(
				'value' => 'its quiet', 
				'decorators' => $this->_elementDecoratorCtl
			)
		);
		
		$this->addElement('text', 'comments', array(
			'label' => 'Comments',
		    'validators' => array(
		    	array('StringLength', false, array(2,50))
			),
		));
		
		$this->addDisplayGroup(
			array('title', 'file', 'dp', 'comments'), 'inputs',
			array(
				'legend' => 'INputs'
			)
		);

		$this->addSubmitReset();
	}
}

Granted there are other solutions, but this works rather well for what I needed - a drop in replacement and the (valid) HTML structure that I wanted.

You should probably want to alter this if you're starting without any legacy stuff hanging over your head.

It should be noted this was written against Zend Framework 1.9.2, there's no guarantee that this will still be valid against future versions or older versions (although it's pretty likely and I've been using Zend_Form with few changes since early 1.x).