MultivalidatableBehavior: Using many validation rulesets per model

By Dardo Sordi (dardosordi)
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:

  1. One for the admin (who has supercow powers)
  2. One for the registration form,
  3. Another for the password change form,
  4. And finally a default.

Model Class:

Download code <?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:

Download code <?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:

Download code
$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:

Download code <?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;
    }

}

?>

 

Comments 619

CakePHP Team Comments Author Comments
 

Comment

1 Thanks

Thank you for a very timely and useful article!
Posted Jun 26, 2008 by Wendy
 

Comment

2 Powerful admin

Haha, I'll have to give our admins "supercow powers" now too! :)
Posted Jul 29, 2008 by Grant Cox.
 

Comment

3 Another Multiple Validation Set Implementation

Jonathan Snook recently published an alternative: http://snook.ca/archives/cakephp/multiple_validation_sets_cakephp/
Posted Jul 30, 2008 by RichardAtHome
 

Comment

4 Using a Behavior

Using a behaviour is really smart. I like it! While I prefer my approach of having it look for a set based on action name (of course, I wrote it!), I think I'll switch it up as a Behavior that I can drop into future projects more easily.
Posted Jul 30, 2008 by Jonathan Snook
 

Comment

5 Problem with this on hosted service ...

I have an application that is working fine on my local machine using
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.
Posted Oct 25, 2008 by Rob Weaver