MultivalidatableBehavior: Using many validation rulesets per model

by 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:

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

}

?>

Report

More on Behaviors

Advertising

Comments

  • jignesh posted on 09/28/12 06:46:19 AM
    This is working fine with me. It's been so nice and this is what i need. But i have a problem, this is working fine upto cakephp 2.2.1 But in 2.2.2 it acts very differently. If you guys can please help me out. Thanks in advance
  • Bodog posted on 08/20/09 01:16:23 AM
    This was exactly what I needed and worked perfectly. Thanks very much.
  • fly2279 posted on 07/18/09 09:24:48 AM
    For a DRYer way of doing things you could specify in the default $validates array all the rules that you want included for all rulesets. I found I was specifying the same rules in multiple sets so I combined the default and the ruleset that I was using by modifying the behavior a little. In the function _setValidation():
    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)
  • webweave posted on 10/25/08 01:54:54 PM
    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.
    • PeterJaap posted on 11/29/09 12:42:01 PM
      @Rob Weaver (#5);

      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 :-)
  • jonathansnook posted on 07/30/08 10:33:24 AM
    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.
  • richardathome posted on 07/30/08 03:45:11 AM
    Jonathan Snook recently published an alternative: http://snook.ca/archives/cakephp/multiple_validation_sets_cakephp/
  • grant_cox posted on 07/29/08 05:20:45 PM
    Haha, I'll have to give our admins "supercow powers" now too! :)
  • Perkster posted on 06/26/08 05:34:53 PM
    Thank you for a very timely and useful article!
login to post a comment.