Today I want to show you a simple, but a very usable concept of validating a form depending on some criteria. There was a time when for one of my projects I needed to create a dynamic form similar to this one:
When checkbox near the field is checked, the field needs to be required, otherwise we just ignore it.
Usually people solve tasks like this by writting alot of code at their controllers, but as I wrote at my previous post this is not the best way to do things. We want to keep our controller as clean as possible and validation task is a task for the form, not for the controller.
But for the cases like one described above we cannot just assign one of the default validators to a form like we do usually. Writting custom validator also does not solve the problem as we need to validate form depending on criteria based on several fields.
So the best way to solve this in my opinion is to override isValid() method at your custom form. Let me first give you the entire code for the form and then I will explain the details below:
class Default_Form_CustomValidation extends Zend_Form { public function init() { $this->setOptions(array( 'elements' => array( 'field_a_enabled' => array( 'type' => 'checkbox', ), 'field_a' => array( 'type' => 'text', 'options' => array( 'label' => 'Field A', ) ), 'field_b_enabled' => array( 'type' => 'checkbox', ), 'field_b' => array( 'type' => 'text', 'options' => array( 'label' => 'Field B', ) ), 'field_c_enabled' => array( 'type' => 'checkbox', ), 'field_c' => array( 'type' => 'text', 'options' => array( 'label' => 'Field C', ) ), 'submit' => array( 'type' => 'submit', 'options' => array( 'label' => 'Save', 'decorators' => array('ViewHelper') ), ), ), 'decorators' => array('FormElements', 'Form'), 'elementDecorators' => array('Label', 'ViewHelper', 'Errors') )); $this->addDisplayGroups(array( 'a' => array('elements' => array('field_a_enabled', 'field_a')), 'b' => array('elements' => array('field_b_enabled', 'field_b')), 'c' => array('elements' => array('field_c_enabled', 'field_c')), 's' => array('elements' => array('submit')) )); $this->setDisplayGroupDecorators(array('FormElements', array('HtmlTag', array('tag'=>'div')))); } public function isValid($data) { $valid_values = $this->getValidValues($data, true); if ($valid_values['field_a_enabled']==='1') { $this->field_a->setRequired(true); } else { $this->field_a->setIgnore(true); } if ($valid_values['field_b_enabled']==='1') { $this->field_b->setRequired(true); } else { $this->field_b->setIgnore(true); } if ($valid_values['field_c_enabled']==='1') { $this->field_c->setRequired(true); } else { $this->field_c->setIgnore(true); } $this->field_a_enabled->setIgnore(true); $this->field_b_enabled->setIgnore(true); $this->field_c_enabled->setIgnore(true); return parent::isValid($data); } }
First, at the init() function we define the fields we need and group it to let it look the way we want. As you can see, each checkbox/input pair is placed into a separate group and each group has “HtmlTag” decorator which wraps them into “div” tag.
This allows us to have checkbox and input placed together, but each new set of those to be placed in a new row.
But the magic of course happens at isValid() function. There I start with this line:
$valid_values = $this->getValidValues($data, true);
getValidValues() validates the form and retieves only the values that passed validation.
At this specific example this line might not seem too useful, and you could use $data array directly to access values, but this is a good practice to always use getValidValues() to retrieve validated values, as it goes through the standard form validation routine for each element here and also filters all the input data from the user, so this adds up to the security of your application.
Ok, so when received a list of valid values, we need to make our custom validation. This is as simple as this for each of our fields:
if ($valid_values['field_a_enabled']==='1') { $this->field_a->setRequired(true); } else { $this->field_a->setIgnore(true); }
What this code says to our application is simply that if a checkbox (field_a_enabled) was checked, then input (field_a) needs to be required, otherwise we just skip it.
For those of you who don’t know what setIgnore() function actually does I will make some explanation as I really like that one. This is quite simple, at your controller when you retrieve your validated values with $form->getValues(true), fields which were marked as ignored do not go to the resulting “values array” regardless to whether POST sent any values for them or not. So this helps to keep things clean and don’t mess up with any values that we don’t need anyway.
What it means in our case is that we’ll only get values for the input fields which were checked with corresponding checkboxes, nothing else. Controller does not need to know anything about checkboxes and it does not need to know about inputs which were not selected (corresponding checkbox not checked). This is the job for the form, and controller will only receive the data it really needs – values from the selected inputs.
When done with custom validation we also set all checkboxes to ignore as they already did their job of letting validation know which fields are required and which are not and we don’t need them further:
$this->field_a_enabled->setIgnore(true); $this->field_b_enabled->setIgnore(true); $this->field_c_enabled->setIgnore(true);
And last, but very importaint line is this:
return parent::isValid($data);
With this line we actually call Zend’s native validation from Zend_Form class as our version of isValid() does not actually validate anything, it just prepares certain fields to be validated by native validation.
So that’s it! That was just an example of what you can do by overriding isValid() and writting custom validation, this gives you a great power to create any really dynamic and flexible validation.
Hope that was useful and I could convince you that placing your custom validation into the form class instead of controller makes your code cleaner and easy to understand.
And as usual I would be glad to hear your thoughts at the comments section.
Thanks!