Custom Form Elements in ZF2: Simple Example


For our final application recode from ColdFusion to PHP, we hit on needing to do some custom form elements.  We’d already done custom form view helpers to implement Bootstrap design (which I’ll cover in-depth as either a separate series or as part of the An Eclectic World Recode series).

This, however, was needing an actual custom element as well, something different.  This application, AgriLife Register, has as part of its feature set the ability for the administrators to build out registration forms for each event, adding questions for registrants to answer.

They have 9 question types they can choose to add to an event.  Most are fairly standard: Texas county select list, single line-text box, multi-line text box, multiple choice pick one (i.e. radios), multiple choice pick many (checkboxes), dollar amount, simple numeric amount, and so forth.  For this, a simple switch statement in the form builder handles creating the appropriate form element.

Two, however, are not standard and need customization: instructions and a multiple choice, pick many, with quantity!  The first is the one we’ll cover today because it was the easier of the two to handle.  The second will be covered in a follow-up post because its gonna take a while 😉

So, instructions.  These are what they sound like, a paragraph or more of textual instructions plopped into the registration form.  These are treated as “questions” in terms of DB and the form handling for many reasons, including how the registration forms are built and the need for them to be sortable with the questions to correctly appear in order on the form.  We also didn’t want to have to do some funky/ugly looping in our views to loop the questions, pull form elements and then flip to just doing text.

In terms of display, it looks like this on the existing ColdFusion site (the “Please select one” text):

InstructionField

In some ways, it is essentially like a hidden field, except there is no field, just the “label”.  So first, we need our custom Instruction form element.  Adding a basic, simple custom element is actually pretty easy.  You need the Element, which we put in Application/Form

<?php
     namespace Application\Form;
     
     use Zend\Form\Element;
     use Zend\InputFilter\InputProviderInterface;
     use Zend\Validator\Regex as RegexValidator;
     
     class Instruction extends Element implements InputProviderInterface {
          // set its type
          protected $attributes = array(
               'type' => 'instruction'
          );
          
          // remove require and validator defaults because we have none
          public function getInputSpecification() {
               return array(
                    'name' => $this->getName(),
                    'required' => false,
                    'validators' => array(),
               );
          }

          // tell it where to find its view helper, so formRow and the like work correctly
          public function getViewHelperConfig() {
               return array('type' => '\Application\Helper\FormInstruction');
          }
     }
?>

That’s it…seriously.  It’s ready to use.  To use it in our form, we just call it same as any other form element:

$oFormElement = new \Application\Form\Instruction($question_name);
$oFormElement->setLabel($sQuestionText);
$this->add($oFormElement);

Note: if you really want to use the short name method, you can, but the process seemed overly complicated and convoluted when this works fine.

In the code above, you can see we extended the basic form Element, then overrode the attributes to add this elements type, removed any default input specifications (since it obviously needs no validation), and pointed the form over to its custom view element.

Which we’ll now create, this time in Application/Helper.

<?php
	namespace Application\Helper;
	
	use Zend\Form\ElementInterface;
	use Zend\Form\View\Helper\AbstractHelper;
	
	class FormInstruction extends AbstractHelper {
		private static $instructionFormat = '
			<div class="col-sm-12">
				<div class="text-muted">
					%s
				</div>
			</div>
			';
		
		public function __invoke(ElementInterface $oElement = NULL) {
			if(!$oElement)
				return $this;
			
			return $this->render($oElement);
		}
		
		public function render(ElementInterface $oElement = NULL) {
			return sprintf(self::$instructionFormat, $oElement->getLabel());	
		}
	}
?>

The view helper includes: the display “template”, the basic invoke which calls the render function, and the render function which basically just says here, shove the label into our template and return it.  Very basic.

However, if you run the above, which is what I first did, the display ended up being not quite right when using formRow…

InstructionViewHelperWrong

This is because Zend doesn’t yet know not to render the label separately.  To change that, we have to modify the formRow and formElement view helpers as well.

Now, we already have a custom formRow helper for our Bootstrap stuff, which I’m not going to dig into now, but for a basic example, you would extend the existing formRow to make a custom one, similar to above, then add a bit of an override to the render function:

<?php
	namespace Application\Helper;

	use Zend\Form\ElementInterface;
	use Zend\Form\View\Helper\FormRow as OriginalFormRow;
	
	class FormRow extends OriginalFormRow {		
		public function render(ElementInterface $oElement, $sLabelPosition = NULL) {
			$sElementContent = $this->getElementHelper()->render($oElement);
			
			if ($sElementType === 'instruction')
				return sprintf($sElementContent);
				
			else
				return parent::render($oElement, $sLabelPosition);
		}
	}
?>

And then the formElement.  Again, we have a fuller custom one, but this is a basic, functional example:

<?php
	namespace Application\Helper;

	use Zend\Form\ElementInterface;
	use Zend\Form\View\Helper\FormElement as OriginalFormElement;
	
	class FormElement extends OriginalFormElement {
		public function render(ElementInterface $oElement) {
			
			$sElementType = $oElement->getAttribute('type');	
			$renderer = $this->getView();
			
			if (!method_exists($renderer, 'plugin'))
     			return NULL;

     		if ($sElementType == 'instruction'):
     			$helper = $renderer->plugin('form_instruction');
     			return $helper($oElement);
     		endif;
			
			return parent::render($oElement);
		}
	}
?>

Make sure to add all three custom helpers to the module.config.php under Application, under the view helpers:

'view_helpers' => array(
	'invokables' => array(
		'formElement' => 'Application\Helper\FormElement',
		'formInstruction' => 'Application\Helper\FormInstruction',
		'formRow' => 'Application\Helper\FormRow',
	),
),

And now we have our basic, functioning view helper that works when called directly or with formRow:

InstructionViewHelperWorking

Weee!  As noted, in a future post I’ll look at the heftier example of a custom form element that is essentially two form fields in one!