Fun with Zend Form Collections


I have had to pleasure to start working with ZF2 Element\Collection the last couple of weeks. Boy can I tell you what fun it is, especially with Zend’s fantastic/useful documentation; can you smell the sarcasm? I did have the benefit of looking at another team members work done in a previous application we built. So I didn’t have to start from scratch, which is always a leg up.

So the problem I needed to solve was a basic setup of questions with Nth amount of options.
Here are the ingredients I used to perform this culinary masterpiece.

  • QuestionEntity (Entity/Bean)
  • OptionEntity (Entity/Bean)
  • QuestionForm (Form)
  • QuestionFilter (InputFilter)
  • OptionsFieldSet (Fieldset implements InputFilterProviderInterface)
  • FormOptionRow (OriginalFormCollection)
  • Editor (View)

The two Entities are your common setup with you have your basic namespace, ‘use’ declarations and variables.

QuestionEntity:

  
namespace Product\Questions;

use Zend\Db\Adapter\Adapter;
use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Db\ResultSet\ResultSet;
use Zend\Db\Sql\Select;
use Zend\Db\Sql\Sql;
use Zend\Stdlib\Hydrator\ClassMethods;

class QuestionEntity {

	protected $question_id;
	protected $question_text;
	protected $display_order;

	// aiding the form submission
	protected $question_options

	// for service interface
	protected $dbAdapter;
	protected $sql = null;
	protected $validator;

	public function __construct(Adapter $dbAdapter) {
		// get our DB adapter and send to the form because the form is 
		// DB dependent in this case
		$this->setDbAdapter($dbAdapter);
		$this->setSQL();				
	}
	
	.....
	
	// aiding the form submission
	// When binding to an exisiting question to the form the Collection is 
	// expecting an array of objects for it to populate with. I check to 
	// see if the question_options have already be set before I grab them 
	// from the database.
	public function getQuestion_Options() {
		
		if (empty($this->question_options) && $this->getQuestion_ID() > 0){
			$aOptions = array();
			
			// Getting options for question
			$oGateway = new QuestionsGateway($this->dbAdapter);
			$oOptions = $oGateway->getAllOptions($this->getQuestion_ID());
			
			foreach ($oOptions as $oResult)
				array_push($aOptions, $oResult);
			
			$this->setQuestion_Options($aOptions);
		}

		return $this->question_options;
	}
	public function setQuestion_Options($question_options) {
		$this->question_options = $question_options;
	}
}

OptionEntity:

 

use Zend\Db\Adapter\Adapter;
use Zend\Db\Sql\Expression;
use Zend\Db\Sql\Select;
use Zend\Db\Sql\Sql;

class OptionsEntity {
	
	protected $option_id;
	protected $option_text;
	protected $fk_question_id;
	protected $display_order;
	
	public function getOption_ID() {
		return $this->option_id;
	}
	public function setOption_ID($option_id) {
		$this->option_id = $option_id;
	}
			
	.....
}

Proceeding to the form, which will have its basic setup along with the addition of the Collection

QuestionForm:

namespace Product\Questions\Forms;

use Zend\Form\Element;
use Zend\Form\Form;

class QuestionForm extends Form {
	private $question_id = null;
	
	public function __construct($questionID) {
		parent::__construct('question-form');

		$this->question_id = $questionID;
		$this
			->setInputFilter(new QuestionFilter())
			->setPreferFormInputFilter(true)
			->setAttributes(array(
				'method' => 'post',
				'class'  => 'form-horizontal standard-form'
			));

		//Form Elements
		$element = new Element\Hidden('question_id');
		$this->add($element);

		$element = new Element\Text('question_text');
		$element
			->setLabel('Question:')
			->setLabelAttributes(array('class' => 'col-sm-3 required'))
			->setAttributes(array(
				'id' => 'question_text',
				'class' => 'pixel-width-500',
				'required' => true
			));
		$this->add($element);

		$fieldset = new \Zend\Form\Element\Collection('question_options');
		$fieldset
			->setOptions(array(
				'allow_add' => true,
				'count' => 3,
				'template_placeholder' => '__optionIndex__',
				'should_create_template' => true,
				'use_as_base_fieldset' => false,
				'target_element' => 
					new \Product\Questions\Forms\OptionsFieldSet(),								
			));
		$this->add($fieldset);

		.....
	}
}

A basic explanation of the options set for the Collection.

  • allow_add: tell Zend that we can append more options
  • count: tells Zend how many rows to display for an empty form i.e. new question
  • template_placeholder: is a unique indicator that we will use to aid with editing
  • should_create_template: allows us to use a template for adding more options
  • use_as_base_fieldset: HAVE NOT FOUND AN EXPLANATION FOR THIS YET
  • target_element: the Entity that will be used to bind to the collection

QuestionFilter:

namespace Product\Questions\Forms;

use Zend\InputFilter\InputFilter;

class QuestionFilter extends InputFilter {

	public function __construct() {
		
		$this->add(array(
			'name' 		=> 'question_id',
			'required' 	=> true,
			'filters' 	=> array(
				array('name' => 'Int'))
		));
		
		....
		
		$this->add(array(
			'name' => 'question_options',
			'required' => true,
		));
	}
}

The fields in question_options will be validated with the instructions we setup below.

OptionsFieldSet:

namespace Product\Questions\Forms;

use Zend\Form\Element;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
use Zend\Stdlib\Hydrator\ClassMethods;

class OptionsFieldSet extends Fieldset 
	implements InputFilterProviderInterface {
	
	public function __construct() {
	
		parent::__construct('question_options');
		
		$this
			->setHydrator(new ClassMethods(true))
			->setObject(new \Product\Questions\OptionsEntity());
		
		$element = new Element\Hidden('option_id');
		$this->add($element);
		
		$element = new Element\Hidden('fk_question_id');
		$this->add($element);
						
		$element = new Element\Text('option_text');
		$element
			->setAttributes(array(
				'class' => 'pixel-width-200 form-control',
			));
		$this->add($element);
	
		.....
	}
	
	public function getInputFilterSpecification() {
		
		return array(
			'option_id' => array(
				'required' => false,
				'filters' => array( 
					array('name' => 'Int') 
				),
			),
			'fk_question_id' => array(
				'required' => false,
				'filters' => array( 
					array('name' => 'Int') 
				),
			),
			'option_text' => array(
				'required' => true,
				'filters' => array(
					array('name' => 'StripTags'),
					array('name' => 'StringTrim'),
					array('name' => 'Null'),
				),
				'validators' => array(
					array(
						'name' => 'StringLength',
						'options' => array(
							'encoding' => 'UTF-8',
							'min' => 2,
							'max' => 255,
							'messages' => array(
								'stringLengthTooShort' => 
	'The name is too short and needs to be at least 2 characters long',
								'stringLengthTooLong' => 
	'The name is too long and needs to be shorter than 256 characters'
							),
						),
					),
				)
			),
			.....
		);
	}
}

The Fieldset is vary similar to setting up a Form & Filter, just make sure you do not forget to add the getInputFilterSpecification.

One more item needs to be created before we can start working on the view

FormOptionRow:

namespace Product\Questions\Helper;
	
	use Zend\Form\Element\Collection;
	use Zend\Form\View\Helper\FormCollection as OriginalFormCollection;
	
	class FormOptionRow extends OriginalFormCollection {
		protected $templateWrapper = 
			'';
		
		public function renderTemplate(Collection $collection) {
			$escapeHtmlAttribHelper = $this->getEscapeHtmlAttrHelper();
        
	   		$templateMarkup = "HTML CODE USED TO INJECT THE NEW OPTIONS"
			
			return sprintf(
				$this->getTemplateWrapper(),
				$escapeHtmlAttribHelper($templateMarkup)
			);
		}
	}

Everything in $templateMarkup will be injected in the span data-template attribute allowing for easy retrieval when appending more options with javascript. After this is created make sure you add this file to your view_helpers in the module.config.php file.

Now we are ready to work on the view.

editor.phml

// assuming proper setup for your form and visual 
// display has already be setup 
.... 

// Grabbing the Collection form the QuestionForm and 
// iterating through the OptionsFieldSet we setup
foreach($form->get('question_options')->getIterator() as $key => $optionsFieldSet){
	
	echo "";
	echo "{$this->formInput($optionsFieldSet->get('option_id'))}";
	echo "{$this->formInput($optionsFieldSet->get('option_text'))}";
	echo "";
}

.....

echo $this->form()->closeTag();
echo $this->formOptionRow()->renderTemplate($panelForm->get('question_options'), $bProduct);
$this->inlineScript()->captureStart();

The options are displayed inside a table named options-table, and in the footer there are two buttons with the class name add-more-options with a data-number or 1 & 5 to indicate the number of loops to perform

		
	function addOptions(event) {
		var nextOptionID = 0;
		var optionsTemplate;
		
		$loop = $(event.target).data('number');
		
		$.each($(".option-row"), function() {
			optionID = $(this).data('id');
			
			if(optionID > nextOptionID)
				nextOptionID = optionID;
		});
		
		for (i = 0; i < $loop; i++){
			nextOptionID++;
			optionsTemplate = $("#rowOptionTemplate").data('template');
			optionsTemplate = 
				optionsTemplate.replace(/__optionIndex__/g, nextOptionID);
			
			$("#options-table tbody").append(optionsTemplate);
		}
	}
		
	$(document).ready(function(){
	
		$(".add-more-options").on("click", function(event){			
			addOptions(event);						
		});
	});

When the user clicks the add more options button at the footer of the table, an event is triggered where the addOptions function will grab the optionsTemplate displayed on line 15 of editor.phml. Line replaces the optionIndex with the next corresponding number, to insure that all options will be pulled through the controller upon post.

Leave a Reply