MultivalidatableBehavior: Using many validation rulesets per model
In this article I present the MultivalidatableBehavior, which allow us to have multiple sets of validation rules for each model.
There are specific situations when we need to change the default validation ruleset for a model. This is exactly what the Multivalidatable behavior does.
First let's see an example of usage.
Suppose we have four sets of validation rules for our user model:
- One for the admin (who has supercow powers)
- One for the registration form,
- Another for the password change form,
- And finally a default.
Model Class:
<?php
Class UserModel extends AppModel {
var $name = 'User';
var $actsAs = array('Multivalidatable');
/**
* Default validation ruleset
*/
var $validate = array(
'realname' => array('rule' => '/[A-Za-z ]+/', 'message' => 'Only letters and spaces please.'),
'username' => array('rule' => 'alphanumeric', 'message' => 'Only letters and numbers please.'),
'password' => array('rule' => array('minLenght', 6), 'message' => 'Password must be at least 6 characters long.'),
'email' => array('rule' => 'email', 'message' => 'Must be a valid email address.'),
);
/**
* Custom validation rulesets
*/
var $validationSets = array(
'admin' => array(
'name' => array('rule' => 'alphanumeric'),
'email' => array('rule' => 'email'),
'age' => array('rule' => 'numeric'),
),
'register' => array(
'realname' => array('rule' => '/[A-Za-z ]+/', 'message' => 'Only letters and spaces allowed, please try again.'),
'username' => array('rule' => 'alphanumeric', 'message' => 'Only letters and numbers, please try again.'),
'password' => array('rule' => array('minLenght', 6), 'message' => 'Password must be at least 6 characters long, please try again.'),
'password_confirm' => array('rule' => 'confirmPassword', 'message' => 'Passwords do not match, please try again.'),
'email' => array('rule' => 'email', 'message' => 'Must be a valid email address.'),
'captcha' => array('rule' => 'checkCaptcha', 'required' => true, 'allowEmpty' => false, 'message' => 'Incorrect validation code, please try again.')
),
'changePassword' => array(
'username' => array('rule' => 'alphanumeric', 'message' => 'Only letters and numbers, please try again.'),
'password' => array('rule' => array('minLenght', 6), 'message' => 'Password must be at least 6 characters long, please try again.'),
'password_confirm' => array('rule' => 'confirmPassword', 'message' => 'Passwords do not match, please try again.')
)
);
function checkCaptcha()
{
// your captcha related code here
}
function confirmPassword()
{
// check that both passwords are equal
}
}
?>
Now in the controller, we can dinamically set the validation ruleset:
Controller Class:
<?php
Class UsersController extends AppController {
var $name = 'Users';
var $scaffold; // I'm lazy today
function beforeFilter() {
parent::beforeFilter();
if (isset($this->params['admin'])) {
// admins have special rules
$this->User->setValidation('admin');
}
}
function register() {
$this->User->setValidation('register');
// here goes the code for registering a new account
}
function password() {
$this->User->setValidation('changePassword');
// here goes the code to allow the users change their own password
}
}
?>
The method setValidation() also accepts as parameter an array with the ruleset:
$this->User->setValidation(array('email' => array('rule' => 'email', 'message' => 'Must be a valid email address')));
Also, there are other utility methods:
restoreValidation() and restoreDefaultValidation() which do exactly what their name implies.
Finally, this is the behavior:
<?php
class MultivalidatableBehavior extends ModelBehavior {
/**
* Stores previous validation ruleset
*
* @var Array
*/
var $__oldRules = array();
/**
* Stores Model default validation ruleset
*
* @var unknown_type
*/
var $__defaultRules = array();
function setUp(&$model, $config = array()) {
$this->__defaultRules[$model->name] = $model->validate;
}
/**
* Installs a new validation ruleset
*
* If $rules is an array, it will be set as current validation ruleset,
* otherwise it will look into Model::validationSets[$rules] for the ruleset to install
*
* @param Object $model
* @param Mixed $rules
*/
function setValidation(&$model, $rules = array()) {
if (is_array($rules)){
$this->_setValidation($model, $rules);
} elseif (isset($model->validationSets[$rules])) {
$this->setValidation($model, $model->validationSets[$rules]);
}
}
/**
* Restores previous validation ruleset
*
* @param Object $model
*/
function restoreValidation(&$model) {
$model->validate = $this->__oldRules[$model->name];
}
/**
* Restores default validation ruleset
*
* @param Object $model
*/
function restoreDefaultValidation(&$model) {
$model->validate = $this->__defaultRules[$model->name];
}
/**
* Sets a new validation ruleset, saving the previous
*
* @param Object $model
* @param Array $rules
*/
function _setValidation(&$model, $rules) {
$this->__oldRules[$model->name] = $model->validate;
$model->validate = $rules;
}
}
?>

Old code:
$model->validate = $rules;New code:$model->validate = Set::merge($this->__defaultRules[$model->name], $rules);Now any rule that is in the default $validates rules gets included with the ruleset that you are using at the time. (inspired by a blog post from teknoid)
the behavior of Multivalidatable
When I run it from my local machine (where I have control of the
httpd.conf), it works just fine.
When I run it on my hosting provider (1and1), I get an error:
Warning (512): SQL Error: 1064: You have an error in your SQL syntax;
check the manual that corresponds to your MySQL server version for the
right syntax to use near 'setvalidation' at line 1 [CORE/cake/libs/
model/datasources/dbo_source.php, line 521
The method in question is in the Multivalidatable.php class under ~/
VolunteerCake/models/behaviors, which seems to be the right place
locally.
My cake directory is at ~/cake, and everything else seems to be
working OK (including the 'Acl' declaration).
BTW, in order to get the app to work on 1and1, I had to modify
the .htaccess files to make the paths absolute as described here:
http://bakery.cakephp.org/articles/view/mod-rewrite-on-godaddy-shared...
As a workaround, I copied the code from your behavior into my User model, modifying it to look like:
/**
* Stores previous validation ruleset
*
* @var Array
*/
var $__oldRules = array();
/**
* Stores Model default validation ruleset
*
* @var unknown_type
*/
var $__defaultRules = array();
function setUp($config = array()) {
$this->__defaultRules[$this->name] = $this->validate;
}
/**
* Installs a new validation ruleset
*
* If $rules is an array, it will be set as current validation ruleset,
* otherwise it will look into Model::validationSets[$rules] for the ruleset to install
*
* @param Object $model
* @param Mixed $rules
*/
function setValidation( $rules = array()) {
if (is_array($rules)){
$this->_setValidation($rules);
} elseif (isset($this->validationSets[$rules])) {
$this->setValidation($this->validationSets[$rules]);
}
}
/**
* Restores previous validation ruleset
*
* @param Object $model
*/
function restoreValidation() {
$model->validate = $this->__oldRules[$this->name];
}
/**
* Restores default validation ruleset
*
* @param Object $model
*/
function restoreDefaultValidation() {
$model->validate = $this->__defaultRules[$this->name];
}
/**
* Sets a new validation ruleset, saving the previous
*
* @param Object $model
* @param Array $rules
*/
function _setValidation($rules) {
$this->__oldRules[$this->name] = $this->validate;
$this->validate = $this;
}
Then I just removed 'Multivalidatable' from the $actsAs variable and everything appears to work again.
Thanks for this solution. There is one problem with your code; it is -ALMOST- correct. You (by accident I presume) changed the last line
$model->validate = $rules;to$this->validate = $this;while you most probably meant;$this->validate = $rules;When I changed 'this' to 'rules', the code worked :-)