Conditional Validation


(Antonio) #1

Currently working on a personal project I was confronting a situation where I had to validate an attribute only if certain conditions where accomplished. The conditions where quite challenging and scenarios weren’t suitable, or maybe I didn’t know how they could be written in order to fulfill my validation requirements, as I had too many conditions to fulfill depending even on the value of certain attributes.

Background:

The attribute ‘A’ was required only if the form was of a certain type

The attribute ‘A’ supposed to be validated only if ‘B’ existed in the form and if ‘B’ fulfilled certain validation requirement. For example, if B existed and also if it had a value=‘EMAIL’, then ‘A’ was to be validated with ‘Email’ validation.

Conditional statements in Controllers should be avoided to keep their code clean and neat, so validators should handle them

My Solution:

a - ) I included attribute ‘A’ to be required at all times

b - ) Created a EConditionalValidator that will only execute a rule if certain validations are passed previously




array('account',

	'application.components.validators.EConditionalValidator',

	'conditionalRules' => array('B', 'compare', 'compareValue' => self::TYPE_EMAIL),

	'rule' => array('A', 'email')

),



c - ) Then, depending on the type of FORM then I validate some attributes or anothers and then save the model without validating further its attributes:




if($_GET['FORM_TYPE'] == FORM_TYPE::TYPE_EMAIL)

	$validationParams = array('A','D', 'C');

else

	validationParams = array('account','A', 'D', 'F');


if ($model->validate($validationParams) && $model->save(false))

	// success


else // failure



Final notes

I would like to know if anybody of you has confront a challenge like this with validation and what it was the solution.

EConditionalValidator is also capable of validating more than one condition before executing one. If its of any use for anyone of you, I would be more than happy to share it.


(Pronk) #2

Not particularly happy with my solution, but…

Whenever a user is created his password is validated with the Length and Compare validation rules.

On update the user can leave his password empty if he does not want to change it. However this caused problems with the Compare validation rule, causing the actual password of the user being validated against the empty password_repeat field, obviously causing an error.

In the end I decided to check if a password has changed (overriding the setAttribute() method). If the password has changed; I add the required validators:


	public function onBeforeValidate(CEvent $event)

	{

    	parent::onBeforeValidate($event);


    	if ($this->password_changed)

    	{  

        	$validator = CValidator::createValidator('length', $this, 'password', array('min' => <img src='http://www.yiiframework.com/forum/public/style_emoticons/default/cool.gif' class='bbc_emoticon' alt='8)' />);

        	$this->getValidatorList()->add($validator);

        	$validator = CValidator::createValidator('compare', $this, 'password');

        	$this->getValidatorList()->add($validator);

    	}

	}



I like your solution better.

And someone please check the spam filter… Its annoying. Had to rewrite this text a few times for it to show up.


(Antonio) #3

Thanks for sharing your solution for a common problem.

Nevertheless, for your challenge, I decided to work creating a new set of attributes: newPassword, and passwordConfirm. Then I set their validation rules as shown below, is easier…




        // attributes

        /**

	 * @var string attribute used for new passwords on user's edition

	 */

	public $newPassword;


	/**

	 * @var string attribute used to confirmation fields

	 */

	public $passwordConfirm;


        // rules

        array('passwordConfirm', 'compare', 'compareAttribute' => 'newPassword', 'message' => 'Entered passwords don\'t match'),

        array('password, newPassword', 'length', 'max' => 50, 'min' => 6, 'tooShort' => 'Password must have at least 6 chars'),




(Yii) #4

I’ve tended to roll inline validators for situation similar to this but it’s rarely been entirely if rule A then run rule B. I think your solution is a good one.


(Pronk) #5

can you elaborate a bit further? When does Password gets updated with Newpassword? Also note that Password of course is a hash, not cleartext.


(Antonio) #6

Is like this:

User already has his password on creation right? Now, we wish to update / hash the password ‘only’ when somebody updates the blank text field right? Then what we do is to create two new attributes (ie newPassword and passwordConfirm) , then we put the already mentioned rules and check onBeforeSave event:




        /**

	 * Hash password if needed

	 */

	public function beforeSave()

	{

		if (parent::beforeSave()) {

			if($this->isNewRecord)

			{

				$this->password = self::hashPassword($this->password);

				$this->validation_key = md5(mt_rand().mt_rand().mt_rand());

			}

			if(!empty($this->newPassword))

			{

				$this->password = self::hashPassword($this->newPassword);

				$this->newPassword = '';

				$this->passwordConfirm = '';

			}

			return true;

		}

		return false;

	}



done


(Marko Bischof) #7

I created a validator in beforeValidate

http://blog.mbischof.de/einen-validator-dynamisch-erzeugen


(Antonio) #8

That is also a fantastic alternative to the same issue. The reason why I didn’t go that way is that I may have to use the same technique in some, not all, models.

Thanks for sharing mbi…


(Mauro Narduzzi) #9

Antonio, is you validator in the extension section od Yii portal?

Many thanks.

Mauro


(Antonio) #10

I did not… wanted to know if people would like to have it… if you think is useful, then I will post it…

Cheers


(Mauro Narduzzi) #11

Yes I’mk interested on it.

But first I explain the problem I’m trying to deal out, but… no way :-[

I have some dependencies in my form and the default validations aren’t able to manage them.

This is my scenario:

  • <province> is required only if <country> has value = 86;

  • <tax_code> is required and should match a regexp only if <country> has value = 86 and only if <user_type> has value = 1;

  • <VAT> is required and should match a regexp only if <country> has value = 86 and only if <user_type> has value = 2;

I’m trying to manage this validation scenario with custom validating function but I do not know how to get the current value of dependency parameters after form submit.

My knowledge of Yii framework is rather low.

Bye.

Mauro


(Adam Klosiu) #12

Ahhh finally someone with similar problem.

I had similar problems while parsing a form with many checkboxes which revealed additional fields. My solution is a little bit different then proposed here.

I created behaviour which allows to run many scenarios at time. Let’s say we have some generic validators for fields which are common for all cases and these are without any scenarios assigned. Then we have some different rules which should be used only when “Option A” checkbox is checked - let’s assign it a scenario “A”. Now, it’s not uncommon to have 2 or more checkboxes in a form which reveals more input fields. For each set let’s create validation rules and assign them a scenarios names “B” and “C”. Now in controller we can check for conditions, and if they are met (in this case if particular checkbox is checked) we can append a scenario to active scenario list. Aforementioned behaviour will make sure all relevant rules are applied.

Example:

ContactForm.php (model)


public function behaviors() {

    	return array(

        	'MetaScenario' => array(

            	'class' => 'application.components.MetaScenario',

            	'mark' => 'META'

        	),

    	);

	}


	/**

 	* Declares the validation rules.

 	*

 	*/

	public function rules() {

    	$rules = array(

        	array('name, surname, mail', 'required'),

        	array('mail', 'email'),

        	array('verifyCode', 'captcha'),

        	// accommodation

        	array('dateFrom, dateTo, people', 'required', 'on' => 'accommodation'),

        	array('people', 'numerical', 'integerOnly' => true, 'on' => 'accommodation'),

        	// equipment

        	array('rentFrom, rentTo', 'required', 'on' => 'equipment'),

    	);

    	$this->createMetaRules(&$rules, $this->getScenario());

    	return $rules;

	}

controller


public function actionContact()

	{

		$contact=new ContactForm;

		if(isset($_POST['ContactForm']))

		{

			$scenario = 'META';

			$scenario .= $_POST['ContactForm']['accommodation']=='Hotel'?'_accommodation':'';

			$scenario .= ($_POST['ContactForm']['equipment']=='Rented')?'_equipment':'';

			$contact->setScenario($scenario);

			$contact->attributes=$_POST['ContactForm'];

			if($contact->validate())

	...

	}



and metaScenario itself:




<?php

class MetaScenario extends CBehavior {

	/**

     *

     * @var string Trigger for meta scenario

     */

	public $mark;

	/**

     * Generate meta scenario based on scenario name

     * @param array $rules Reference to rules array should be passed.

     * @param string $scenario Scenario string. In model use $this->getScenario()

     * @return boolean

     */

	public function createMetaRules($rules,$scenario){

		if (strpos($scenario, $this->mark)===FALSE){

			return false;

		}

		$metaList = $this->createMetaList($scenario);

		//check for rules from scenario list and add them to main rule array if found

		foreach ($rules as &$rule)

		{

			if (array_key_exists('on', $rule))

			{

				if(is_array($rule['on'])){

					$on=$rule['on'];

				}else{

					$on=preg_split('/[\s,]+/',$rule['on'],-1,PREG_SPLIT_NO_EMPTY);

				}

				$tmp = array_intersect($metaList, $on);

				if(!empty($tmp)){

					array_push($on, $scenario);

					$rule['on'] = $on;

				}

			};

		}

		return true;

	}

	

	/**

     * Create scenario list and gets rid of mark

     * @param string $scenario Scenario string.

     * @return array scenarios list.

     */

	private function createMetaList($scenario)

	{

		$metaList = explode('_',$scenario);

		$metaList = array_slice($metaList, 1);

		return $metaList;

	}

}

?>



of course this is a limited example and only relevant bits of model and controller are shown. I wrote that some time ago and now I can see lot’s of room for improvement :). In case anyone is interested I’ll be happy to improve it a bit.


(Antonio) #13

Even though I think EConditionalValidator would help you out as it seems to me that with CCompareValidator plus the CRequiredValidator and CRegularExpressionValidator will be solved easily, I believe that for your issue is better to have a solution like MBI exposed: create a validator in beforeValidate

http://blog.mbischof…amisch-erzeugen and you will implement it easier…

You create a beforeValidate procedure on your model and check for those values to do the regex :)

If you have problems doing that, let me know, I will polish the code of the validator, publish it and use your problem to use it in the wiki :)

Cheers


(Mauro Narduzzi) #14

:( Uhmm, yes I have some problem. Moreover the MBI solutions now seems to be offline (database connection error).

Mauro


(Antonio) #15

I will publish it as an extension then…


(Antonio) #16

I am about to publish the EConditionalValidator, please check if the following will fit your requirements.




// assuming you have the validator on your protected/components/validators/ folder

public function rules(){

   $cv = 'application.components.validators.EConditionalValidator';

   return array(

         array(

             'province', $cv,

             'conditionalRules' => array('country', 'compare', 'compareValue' => 86),

             'rule' => array('required')

         ),

         array(

             'tax_code', $cv,

             'conditionalRules' => array(

                 'group'=>array(

                     array('country', 'compare', 'compareValue' => 86),

                     array('user_type', 'compare', 'compareValue' => 1)

                  ),

              ),

              'rule'=>array('match', 'pattern'=>'/^([a-z0-9_])+$/'),

         ),

         array(

             'VAT', $cv,

             'conditionalRules' => array(

                 'group'=>array(

                     array('country', 'compare', 'compareValue' => 86),

                     array('user_type', 'compare', 'compareValue' => 2)

                  ),

              ),

              'rule'=>array('match', 'pattern'=>'/^([a-z0-9_])+$/'),

         ),

   );

}



PS: Change the pattern for the one you need.


(Mauro Narduzzi) #17

Yes, it is right. Obvioulsy I will put the right patterns where needed.

Meanwhile I have found a “partial” solutions using a fast implentation certainly not well formed. But my limited knowledge of Yii framework stop me every line of code :unsure:

The idea is to use a validation function passing as parameters the default validation metod (required, email, and so on) and a structure the defines the dependencies (e.g. (country:86;user_type:2) that will be parsed into the validating function.

But your solution seems to be very clean and useful, not only for my projects I mean :)


(Antonio) #18

Yeah… you can use a custom validation function… as with beforeValidate but, like this you can have more well formed way of validating attributes…


(Mauro Narduzzi) #19

I was trying the extension. When there are no nested group rules is all ok, but when I declare the group array something goes wrong.

This is my rules array:




Array

(

    [0] => Array

        (

            [0] => province

            [1] => EConditionalValidator

            [conditionalRules] => Array

                (

                    [0] => country

                    [1] => compare

                    [compareValue] => 86

                )


            [rule] => Array

                (

                    [0] => required

                )


        )


    [1] => Array

        (

            [0] => taxcode

            [1] => EConditionalValidator

            [conditionalRules] => Array

                (

                    [group] => Array

                        (

                            [0] => Array

                                (

                                    [0] => country

                                    [1] => compare

                                    [compareValue] => 86

                                )


                            [1] => Array

                                (

                                    [0] => usertype

                                    [1] => compare

                                    [compareValue] => 1

                                )


                        )


                )


            [rule] => Array

                (

                    [0] => required

                )


        )


    [2] => Array

        (

            [0] => company

            [1] => EConditionalValidator

            [conditionalRules] => Array

                (

                    [group] => Array

                        (

                            [0] => Array

                                (

                                    [0] => country

                                    [1] => compare

                                    [compareValue] => 86

                                )


                            [1] => Array

                                (

                                    [0] => usertype

                                    [1] => compare

                                    [compareValue] => 2

                                )


                        )


                )


            [rule] => Array

                (

                    [0] => required

                )


        )


    [3] => Array

        (

            [0] => vat

            [1] => EConditionalValidator

            [conditionalRules] => Array

                (

                    [group] => Array

                        (

                            [0] => Array

                                (

                                    [0] => country

                                    [1] => compare

                                    [compareValue] => 86

                                )


                            [1] => Array

                                (

                                    [0] => usertype

                                    [1] => compare

                                    [compareValue] => 2

                                )


                        )


                )


            [rule] => Array

                (

                    [0] => required

                )


        )


)



For simplicity I declared them as required.

I get the following error during validation:

PHP Error

Undefined offset: 1

/data/www/meade/protected/components/validators/EConditionalValidator.php(111)

This is the line of code that return PHP error:


list($attributes, $conditionalValidator) = $rule;

I think that you should modify validateConditional function like this:




protected function validateConditional(&$object, $rule) {            

            if (isset($rule['group'])) {

             ...

            } else {

                list($attributes, $conditionalValidator) = $rule;

                ...

            }

	}



otherwise list function throws an error after recursion.

Mauro


(Antonio) #20

I think you got the wrong version… that line was deleted… please, let me check

You were right :), nevertheless, could you paste also your rules here? Not the array, the actual rules

PS: I have updated the version and fixed… thanks Mauro…