Conditional Form Validation with Zend_Form
Posted on December 24th, 2008 in Development | 12 Comments »
A question from ‘ronny stalker’ in the Zend_Form_Element_Multi – Tips and Tricks comments:
I need to do different validations for field A depending on the value of field B and (possibly depending on a variable that is not in the form at all – C ).
in this kind of logic:
if (B ==1)
{
validator_B(A);
}
elseif (C)
{
validator_C(A);
}
else
{
validator_Default(A);
}I understand that validators get a secondary argument called $context – which can be used to check values of other fields, but how can a validator get knowledge of other variables in the environment?
While this post may not answer ronny’s question exactly, hopefully it will give him a good starting point to get over the hump.
If other, please explain – Conditional Validation Using $context
Many forms have a set of radio buttons, or sometimes a select element, where a user can choose from one of several options. Sometimes “other” will be one of those options, with a corresponding “If other, please explain” text field placed directly after. If “other” is selected, then the accompanying text field is usually required. Since there’s not a standard Zend Validate validator for this scenario, I’ve written a custom validator that seems to do the trick.
<?php /** * Kendall Extensions * * @category Kendall * @package Kendall_Validate * @author Jeremy Kendall */ /** * @see Zend_Validate_Abstract */ require_once 'Zend/Validate/Abstract.php'; /** * Requires field presence based on provided value of radio element. * * Example would be radio element with Yes, No, Other option, followed by an "If * other, please explain" text area. * * IMPORTANT: For this validator to work, allowEmpty must be set to false on * the child element being validated. * * From Zend Framework Documentation 15.3: "By default, when an * element is required, a flag, 'allowEmpty', is also true. This means that if * a value evaluating to empty is passed to isValid(), the validators will be * skipped. You can toggle this flag using the accessor setAllowEmpty($flag); * when the flag is false, then if a value is passed, the validators will still * run." * * @uses Zend_Validate_Abstract * @category Kendall * @package Kendall_Validate * @author Jeremy Kendall */ class Kendall_Validate_FieldDepends extends Zend_Validate_Abstract { /** * Validation failure message key for when the value of the parent field is an empty string */ const KEY_NOT_FOUND = 'keyNotFound'; /** * Validation failure message key for when the value is an empty string */ const KEY_IS_EMPTY = 'keyIsEmpty'; /** * Validation failure message template definitions * * @var array */ protected $_messageTemplates = array( self::KEY_NOT_FOUND => 'Parent field does not exist in form input', self::KEY_IS_EMPTY => 'Based on your answer above, this field is required', ); /** * Key to test against * * @var string|array */ protected $_contextKey; /** * String to test for * * @var string */ protected $_testValue; /** * FieldDepends constructor * * @param string $contextKey Name of parent field to test against * @param string $testValue Value of multi option that, if selected, child field required */ public function __construct($contextKey, $testValue = null) { $this->setTestValue($testValue); $this->setContextKey($contextKey); } /** * Defined by Zend_Validate_Interface * * Wrapper around doValid() * * @param string $value * @param array $context * @return boolean */ public function isValid($value, $context = null) { $contextKey = $this->getContextKey(); // If context key is an array, doValid for each context key if (is_array($contextKey)) { foreach ($contextKey as $ck) { $this->setContextKey($ck); if(!$this->doValid($value, $context)) { return false; } } } else { if(!$this->doValid($value, $context)) { return false; } } return true; } /** * Returns true if dependant field value is not empty when parent field value * indicates that the dependant field is required * * @param string $value * @param array $context * @return boolean */ public function doValid($value, $context = null) { $testValue = $this->getTestValue(); $contextKey = $this->getContextKey(); $value = (string) $value; $this->_setValue($value); if ((null === $context) || !is_array($context) || !array_key_exists($contextKey, $context)) { $this->_error(self::KEY_NOT_FOUND); return false; } if (is_array($context[$contextKey])) { $parentField = $context[$contextKey][0]; } else { $parentField = $context[$contextKey]; } if ($testValue) { if ($testValue == ($parentField) && empty($value)) { $this->_error(self::KEY_IS_EMPTY); return false; } } else { if (!empty($parentField) && empty($value)) { $this->_error(self::KEY_IS_EMPTY); return false; } } return true; } /** * @return string */ protected function getContextKey() { return $this->_contextKey; } /** * @param string $contextKey */ protected function setContextKey($contextKey) { $this->_contextKey = $contextKey; } /** * @return string */ protected function getTestValue () { return $this->_testValue; } /** * @param string $testValue */ protected function setTestValue ($testValue) { $this->_testValue = $testValue; } }
The validator above is essentially a conditional NotEmpty validator. It checks the value of a parent field to see if a child field should be required. IMPORTANT: allowEmpty must be set to false on the child field.
Here’s an example of how to use the validator.
// Parent element $this->addElement('radio', 'flavor', array( 'required' => true, 'label' => 'Choose a flavor', 'multiOptions' => array('Vanilla' => 'Vanilla', 'Chocolate' => 'Chocolate', 'Other' => 'Other') )); // Child element. IMPORTANT: allowEmpty must be set to false! $this->addElement('text', 'flavorOther', array( 'allowEmpty' => false, 'label' => 'If Other, provide flavor here', 'validators' => array(new Kendall_Validate_FieldDepends('flavor', 'Other')), ));
Again, please note that allowEmpty has been set to false on the child field. This is necessary to run the FieldDepends validator even when the “If other . . .” element is empty.
While I’m sure there’s plenty of room for refactoring, the above code has served me well.
Adding Validators After Submission but Before Validation
Expanding on the example above, what if it became necessary to add additional validators to the “If other . . .” field? Because the “If other . . .” field has allowEmpty set to false, and because an empty value is sometimes a valid value, it is not possible to add additional validators that will run only if the field is not empty. The additional validators will run regardless of the value of the “If other . . .” element, throwing errors when the element is empty. Additional validators will have to be added somewhere else.
In order to work around this issue, I added a custom method called preValidation() to my form class.
public function preValidation($data) { if (!empty($data['flavorOther'])) { $this->flavorOther->addValidator(new FlavorOther_Validator()); } return $data; }
The preValidation() method is called after submission but before validation.
$form = new Flavor_Form(); if (!$this->getRequest()->isPost()) { // Display form $this->view->form = $form; return; } $data = $form->preValidation($_POST); if (!$form->isValid($data)) { // Failed validation, redisplay form with values and errors $this->view->form = $form; return; } // Passed validation
While the preValidation() code above adds validation depending on the state of an element in the form, it would be trivial to add validation to the form based on any number of conditions, including conditions that exist as a result of business rules rather than the form’s input.
Wrapping Up
Writing custom validators for the Zend Framework makes server side validation of unique validation scenarios a breeze. I have yet to encounter a non-standard validation scenario where I haven’t been able to address it by writing a custom validator. With the ability to extend Zend Form with a couple of helpful custom methods, adding additional validation after form submission becomes trivial.
Have you ever had to write any custom validators? Any suggestions on improving the code above? Jump down to the comments and let us know!
UPDATED to add code comments to the validator implementation example. Thanks to reader Neil for the suggestion.









12 Responses
thanks for that – once again, it is a handy tip.
Hey all,
thanks for that nice validator I was looking longtime for. Great job. I have one more question: If i try to use it for an file_upload element. It does not work. The Problem ist that the $context is null. so that there is no array where you can check if the contextkey is in. If I send a file -> the $context is the well known $file array…. So till now I could not realize to find this solution above if a file upload is required when the radio button is set to “Upload a File”. Any Ideas? Especially i found out that the isValid method is not started when no file is choosen. Thanks for some comments
A~
@Andreas I’ve never dealt with that before, so I don’t have a good answer for you. I suggest heading over to Nabble and asking the ZF community for some recommendations.
I can tell that this is not the first time at all that you write about the topic. Why have you chosen it again?
My situation is a little different because the customer wants to be able to change validators field by field at runtime. I wrote a “Not Duplicate” custom validator that depends on a form_fieldDuplicate() routine being in the model. To do conditional validators I’ll either have to hard code this validator or set up a way to select the parent field, perhaps in a dropdown. Either way, thanks for the article.
I want something exactly like this, but I don’t think I’ll be able to use yours. It seems to implement the letter of Zend_Validate_Interface, but not the spirit. ZVI requires the function isValid($value). You implemented that with isValid($value, $context=null). It technically implements the interface, but whenever you call that function with a null context, it errors out. So you can’t use this with something like Zend_Filter_Input that hides the isValid call from you.
Another way to solve this problem is oveload method Zend_Form::isValid for example:
public function isValid($data)
{
// First validation
$result = parent::isValid($data);
// Add optional second validator if first pass form is ok
if ($result == true and $this->getValue(‘type’)==’MEM’) {
$element = $this->getElement(‘type’);
$element->addValidator(new Zend_Validate_Date());
$result = parent::isValid($data);
}
return $result;
}
The problem i had was that my custom validator was not being called when the value of the input (select) was empty. I couldnt use setRequired() because in some cases (dependent upon another form value) i want to process empty values.
The solution was to do the following:
$organiser_member = new Zend_Form_Element_Text(‘organiser_member’);
$organiser_member->setLabel(‘* Organiser:’);
// Force empty values through the custom validator!
$organiser_member->setRequired(false);
$organiser_member->setAllowEmpty(false);
// Add the custom validator
$organiser_member->addPrefixPath(‘BC_Validate’, ‘BC/Validate’, ‘validate’);
$organiser_member->addValidator(‘EventOrganiser’, false);
$form->addElement($organiser_member);
Now the custom validator is always called, whether the input ‘organiser_member’ is set or not.
Great Script.
Thanks
Had a bit of trouble with it at first as I hadn’t read the write up properly and missed the bit about setting the allowEmpty flag on the validated element. Very simple to use once this is in place.
Yeah, that allowEmpty can be a bear. Glad to hear it’s working for you.
after a lot of tries I give up with this validator and simple put in the preValidation() method:
if ($data['datatype'] == ‘radio’) {
$this->option_1->setRequired(true);
}
where datatype is my parent field and radio is the value that need the check if child option_1 field value is not empty.
What do you think about?
Extremely wonderful desgin of this site. It’s individual and compares for a posts. Don´t give up and make your personal issue!