- Easy approach: inside-model rule
- Complete approach: extending the CValidator class
- Creating the class file
- Implementing Client Validation
- Last step: how to use your validation class inside the module rules
- Links
Some times the core validation rules provided by Yii won't satisfy all your needs, so you'll need to create your very own validation rule.
Easy approach: inside-model rule ¶
The easiest way to create a new validation rule is inside the model that is going to use it.
Let's say that you want to check if a user password is safe enough.
Usually you could achieve this result just by using the CRegularExpressionValidator but for the sake of this guide let's pretend that validator does not exist.
first of all in your model class you'll have to add two constants
const WEAK = 0;
const STRONG = 1;
then in your rules method you'll have to set the rule
/**
* @return array validation rules for model attributes.
*/
public function rules()
{
return array(
array('password', 'passwordStrength', 'strength'=>self::STRONG),
);
}
make sure that you won't give the rule the name of an existing one, otherwise you are going to have some troubles later.
Now the only thing you need to do is create a new method inside the model, named after the validation rule you just declared.
/**
* check if the user password is strong enough
* check the password against the pattern requested
* by the strength parameter
* This is the 'passwordStrength' validator as declared in rules().
*/
public function passwordStrength($attribute,$params)
{
if ($params['strength'] === self::WEAK)
$pattern = '/^(?=.*[a-zA-Z0-9]).{5,}$/';
elseif ($params['strength'] === self::STRONG)
$pattern = '/^(?=.*\d(?=.*\d))(?=.*[a-zA-Z](?=.*[a-zA-Z])).{5,}$/';
if(!preg_match($pattern, $this->$attribute))
$this->addError($attribute, 'your password is not strong enough!');
}
The new method you just created accepts two arguments:
- $attribute = is the name of the attribute that the method is validating
- $params = additional parameters that you could define in the rules
In our rules method we used this rule on the password attribute, so the value of attribute inside our validation model will be password
In the rule we also setted an additional parameter named strength
the value of that parameter will be inside the $params array
As you can see inside the method we are making a call to CModel::addError().
Add Error accepts two parameters: the first one is the name of the attribute that you want to display the error in your form, the second one is the actual error string you want to be displayed.
Complete approach: extending the CValidator class ¶
If you need your custom validation rule in more then one model the best thing to do is extending the CValidator class.
Extending this class you also can take advantage of other features, like CActiveForm::$enableClientValidation, first implemented with Yii 1.1.7 release.
Creating the class file ¶
The first thing that you have to do is create your class file. The best thing is to always name it after your class name, to best use Yii lazy loading feature.
Let's create a new directory inside your application extensions directory (which is located inside the protected directory).
Name this directory MyValidators.
Then we create our own file: passwordStrength.php
Inside this file create our CValidator class
class passwordStrength extends CValidator
{
public $strength;
private $weak_pattern = '/^(?=.*[a-zA-Z0-9]).{5,}$/';
private $strong_pattern = '/^(?=.*\d(?=.*\d))(?=.*[a-zA-Z](?=.*[a-zA-Z])).{5,}$/';
...
In the class file create one attribute for each additional parameter that you want to use inside your validation rule.
CValidator will take care to populate that attribute with the parameter value all by itself.
We also created two other attributes, each containing the patterns we want to use in our preg_match function.
Now we have to override the parent abstract method validateAttribute
/**
* Validates the attribute of the object.
* If there is any error, the error message is added to the object.
* @param CModel $object the object being validated
* @param string $attribute the attribute being validated
*/
protected function validateAttribute($object,$attribute)
{
// check the strength parameter used in the validation rule of our model
if ($this->strength == 'weak')
$pattern = $this->weak_pattern;
elseif ($this->strength == 'strong')
$pattern = $this->strong_pattern;
// extract the attribute value from it's model object
$value=$object->$attribute;
if(!preg_match($pattern, $value))
{
$this->addError($object,$attribute,'your password is too weak!');
}
}
The method above is self explanatory i think.
Of course you could use constants in those IF, and I actually recommend it.
Implementing Client Validation ¶
If you want to implement client validation you'll need to override another method inside your class: clientValidateAttribute
/**
* Returns the JavaScript needed for performing client-side validation.
* @param CModel $object the data object being validated
* @param string $attribute the name of the attribute to be validated.
* @return string the client-side validation script.
* @see CActiveForm::enableClientValidation
*/
public function clientValidateAttribute($object,$attribute)
{
// check the strength parameter used in the validation rule of our model
if ($this->strength == 'weak')
$pattern = $this->weak_pattern;
elseif ($this->strength == 'strong')
$pattern = $this->strong_pattern;
$condition="!value.match({$pattern})";
return "
if(".$condition.") {
messages.push(".CJSON::encode('your password is too weak, you fool!').");
}
";
}
As you can see this method simply returns the javascript that you need to use for your validation
Last step: how to use your validation class inside the module rules ¶
There are several approach you can use here.
You could first use Yii::import in the rules method before returning the rules array, or you can just use Yii dot notation:
/**
* @return array validation rules for model attributes.
*/
public function rules()
{
return array(
array('password', 'ext.MyValidators.passwordStrength', 'strength'=>self::STRONG),
);
}
Links ¶
- Russian version The PHP Times
best practice
Is there no way to globally create 5 validation methods in 1 class? It seems like such a waste to create 5 classes to get 5 reusable validation methods...
I'd like the exact same format as you would create a validation method within your Model/AR. Only more and reusable.
more validation rules in one class
well you could, just use one of the rule parameters to decide which rule apply from time to time.
I did that in the above example, but keep in mind that while it's fine for the wiki example that's not always a good practice.
if you use that kind of system to put together a bunch of totally different rules the rule name won't be intuitive and you lose one of the key advantages of lazy loading.
You use one class for validation rule for the sake of modularity
multiple attributes
How can I make and inside-model validation function that takes more than one attributes?
In my case I have to pass hours and minutes and check that at least one of them is a positive number (the time is more than 00:00).
valid JavaScript patterns
When using patterns other than the ones described in this article, make sure you have them in this format '/ .... /' (e.g. private $pattern = '/#.^(?=.{6,45})(?=.[a-z])(?=.[A-Z])(?=.[0-9]).*$#/'; ) if you want to implement client validation.
note:
what means self::STRONG ? i didn't find that constant in code
behaviors?
On the matter of reusability, does anyone believe that attaching a behavior to the model would be a good practice?
Of course I mean that the behavior contains the public methods that are going to be used as custom validators.
Note: I haven't tested it yet to see if it is possible..
Need to optimize thise code for reusabilitiy.
i am much agree with this.
rudiedirkx at 2011/03/31 01:47pm
best practice
i am also planning to create a this kind of validator class in which we can define a some global functions for more model validation rules.
if anybody is already gone for this approach .then it would be great for this community.
Thank you!
Very good approach, thanks!
Thanks
Thanks for this nice article... helped me alot.
Good Approach
nice work
Ternary operator
I think, that first four lines of
passwordStrength
code (in the easy approach) can be reduced to one, using ternary operator:$pattern = ($params['strength'] === self::WEAK) ? '/^(?=.*[a-zA-Z0-9]).{5,}$/' : '/^(?=.*\d(?=.*\d))(?=.*[a-zA-Z](?=.*[a-zA-Z])).{5,}$/';
Simple validation component
I created a simple component to handle date validation without the need to define additional constants in the model:
/* model code */ public function rules() { return array( array('login','application.components.CDateValidator','dateFormat'=>'datetime'), ); }
/* file: components/CDateValidator.php */ class CDateValidator extends CValidator { public $dateFormat; private $datetime_pattern = '~^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}\:[0-9]{2}\:[0-9]{2}$~'; private $date_pattern = '~^[0-9]{4}-[0-9]{2}-[0-9]{2}$~'; protected function validateAttribute($object,$attribute) { if ($this->dateFormat == 'datetime') $pattern = $this->datetime_pattern; elseif ($this->dateFormat == 'date') $pattern = $this->date_pattern; if(!preg_match($pattern,$object->$attribute) and $object->$attribute!='' and $object->$attribute!=null) { $attrLabels = $object->attributeLabels(); $object->addError($attribute, Yii::t('/models/global','model.validator.date', array('{attributeName}'=>$attrLabels[$attribute]))); } } }
I could make my own validator thanks to this guide!
Thanks to this tutorial I could make my own validator, now rresiding at https://github.com/ktaris/yii-mexvalidators
Again, thanks for the guide!
Simple validation rule
/* model code */ public function rules() { return array( array('DateEnd', 'validatorCompareDateTime', 'compareAttribute' => 'DateStart', 'condition' => '>'), ); } public function validatorCompareDateTime($attribute, $params) { $compareAttribute = $params['compareAttribute']; $condition = $params['condition']; if ($this->hasErrors($attribute) || $this->hasErrors($compareAttribute) || empty($this->$compareAttribute)) { return; } $validateValue = new DateTime($this->$attribute, new DateTimeZone(Yii::app()->getTimeZone())); $compareValue = new DateTime($this->$compareAttribute, new DateTimeZone(Yii::app()->getTimeZone())); switch ($condition) { case '>=': if (($validateValue >= $compareValue) === false) { $this->addError($attribute, sprintf('The value in the "%s" field must be greater than or equal to the value in the "%s" field', $this->getAttributeLabel($attribute), $this->getAttributeLabel($compareAttribute))); } break; case '>': if (($validateValue > $compareValue) === false) { $this->addError($attribute, sprintf('The value in the "%s" field must be greater than the value in the "%s" field', $this->getAttributeLabel($attribute), $this->getAttributeLabel($compareAttribute))); } break; } }
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.