How to use multiple validation rule sets per model in cakephp

As default we have single var $validate rule set in the model class which is triggered on each save() method call while saving form data. This is quite logical and sufficient for single form fields to validate but what if we have another form with somewhat different fields which we want to post and validate? As an answer i know of three different approaches.

Approach One

As per the latest release (not sure about previous releases) cakephp treats model validations “if and only if” way. In other words, a field will only be validated if it is present in the $this->data data set and not flaged as ‘require’=>true in the var $validate rule set. This approach may come handy when there is small number of forms (1-2) and also small number of fields for each form.

I will try to explain it using examples. Let us assume we have a registration process broken down in two steps. Below is the step one form view.

echo $form->create("RegisterStep1",array('controller'=>'Users','action'=>'Register1'));
echo $form->input('User.firstname');
echo $form->input('User.lastname');
echo $form->input('User.email');
echo $form->input('User.password');
echo $form->input('User.password_confirm');
echo $form->submit('  Next Step  ');
echo $form->end();

And step two is:

echo $form->create("RegisterStep2",array('controller'=>'Users','action'=>'Register2'));
echo $form->input('User.city');
echo $form->input('User.state');
echo $form->select('User.country',$countries);
echo $form->input('User.phone');
echo $form->submit('Finish Registration');
echo $form->end();

here is the model validation rule set:

var $validate = array(
	'firstname' => array('rule' => '/[A-Za-z ]+/', 'message' => 'Firstname must contain letters and spaces only.'),
	'lastname' => array('rule' => '/[A-Za-z ]+/', 'message' => 'Lastname must contain letters and spaces only.'),
	'email' => array('rule' => VALID_EMAIL, 'message' => 'Please enter a valid email address.')
	'password' => array('rule' => array('minLenght', 6), 'message' => 'Password must be at least 6 characters long.'),
	'password_confirm' => array('rule' => 'confirmPassword', 'message' => 'Passwords do not match.'),
	'city' => array('rule' => '/[A-Za-z ]+/', 'message' => 'City name must contain letters and spaces only.'),
	'state' => array('rule' => '/[A-Za-z ]+/', 'message' => 'State name must contain letters and spaces only.'),
	'country' => array('rule' => VALID_NOT_EMPTY, 'message' => 'You must select a country.'),
	'phone' => array('rule' => 'phone', 'message' => 'Please enter a phone number.'),
);

and finally controller functions. Registration Step 1 submits data to Register1 function which looks like this :

function Register1()    {
	//other logic
	if(!empty($this->data))    {
		$this->User->save($this->data);
	}
	//other logic
}

and Register2 like this one.

function Register2()    {
	//other logic
	if(!empty($this->data))    {
		$this->User->save($this->data);
	}
	//other logic
}

A simple and straight forward approach! But, someone with a habit of keeping code neat and clean may not like this way as it would mix up the validation rules for all forms and also it may become quite messed up with the increasing number of forms and/or form fields. So we need to start thinking about a different way which would provide a somewhat independent and separate validation rules. I myself adopted this approach (two) until i came across the third one (explained later).

Approach Two

In one of my cakephp projects i had to break a large form into two different forms and then to save them to a single model. My form views remained same as exampled above but what i did, i created a dummy model “Register” with $useTable=”users”. Now i had two models one User and other Register with the following sampled lines of code respectively.

class User extends AppModel{

	var $name="User";

	var $validate = array(
		'firstname' => array('rule' => '/[A-Za-z ]+/', 'message' => 'Firstname must contain letters and spaces only.'),
		'lastname' => array('rule' => '/[A-Za-z ]+/', 'message' => 'Lastname must contain letters and spaces only.'),
		'email' => array('rule' => VALID_EMAIL, 'message' => 'Please enter a valid email address.')
		'password' => array('rule' => array('minLenght', 6), 'message' => 'Password must be at least 6 characters long.'),
		'password_confirm' => array('rule' => 'confirmPassword', 'message' => 'Passwords do not match.')
	);

//custom functions like confirmPassword etc. here
}

and

class Register extends AppModel{

	var $useTable="users";

	var $validate = array(
		'city' => array('rule' => '/[A-Za-z ]+/', 'message' => 'City name must contain letters and spaces only.'),
		'state' => array('rule' => '/[A-Za-z ]+/', 'message' => 'State name must contain letters and spaces only.'),
		'country' => array('rule' => VALID_NOT_EMPTY, 'message' => 'You must select a country.'),
		'phone' => array('rule' => 'phone', 'message' => 'Please enter a phone number.')
	);
}

Hold on! As I get form data in $this->data[‘User’] format i need to hack data inside my controller’s Register2 function so i could submit that data to $this->Register model object.

Here is the alteration in Register2:

function Register2()    {
	//other logic
	if(!empty($this->data))    {
		$this->data['Register'] = $this->data['User'];//passing User data to Register
		if($this->Register->save($this->data))    {
		//logic
		}    else    {
		$this->data['User'] = $this->data['Register'];//need to pass Register data back to User so we can show errors etc.
		}
	}
	//other logic
}

And it worked. I know it was a cheap hack but i didn’t have much time to think about the third one (behaviour thing) and my forms were so big that i could not use the first one, it worked just fine for me.

Approach Three

Use of MultivalidatableBehavior by Dardo Sordi at bakery.cakephp.org seems perfect for all model validation needs. One can set different validation rule sets in a single model class and call whichever necessary. By placing the main logic in behaviour makes it portable and accessible to every model class.

Ok. Now we will see it in the context of our broken registration process. Here is the example source code of our User model class:

Class UserModel extends AppModel {

	var $name = 'User';

	var $actsAs = array('Multivalidatable');

	/**
	* Default validation rule set
	*/
	var $validate = array(
		'fieldname' => array('rule' => 'rulehere', 'message' => 'message here.')
		//more validation rules here
	);

	/**
	* Custom validation rules sets
	*/
	var $validationSets = array(
		'register1' => array(
			'firstname' => array('rule' => '/[A-Za-z ]+/', 'message' => 'Firstname must contain letters and spaces only.'),
			'lastname' => array('rule' => '/[A-Za-z ]+/', 'message' => 'Lastname must contain letters and spaces only.'),
			'email' => array('rule' => VALID_EMAIL, 'message' => 'Please enter a valid email address.')
			'password' => array('rule' => array('minLenght', 6), 'message' => 'Password must be at least 6 characters long.'),
			'password_confirm' => array('rule' => 'confirmPassword', 'message' => 'Passwords do not match.')
		),
		'register2' => array(
			'city' => array('rule' => '/[A-Za-z ]+/', 'message' => 'City name must contain letters and spaces only.'),
			'state' => array('rule' => '/[A-Za-z ]+/', 'message' => 'State name must contain letters and spaces only.'),
			'country' => array('rule' => VALID_NOT_EMPTY, 'message' => 'You must select a country.'),
			'phone' => array('rule' => 'phone', 'message' => 'Please enter a phone number.')
		)
	);

	//custom function like confirmPassword etc. here
}

And here are controller functions:

function Register1()    {
	//other logic
	if(!empty($this->data))    {
		$this->User->setValidation('register1');//calling specific validation only
		$this->User->save($this->data);
	}
	//other logic
}

and Register2:

function Register2()    {
	//other logic
	if(!empty($this->data))    {
		$this->User->setValidation('register2');//calling specific validation only
		$this->User->save($this->data);
	}
	//other logic
}

Isn’t a more precise and clean approach? Furthermore, 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'))); 

And finally here is the source code of behaviour class

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

And thats all for multiple validation rule sets for a single model! You certainly are very patient to be with me its being a long post! Thank You.

5 thoughts on “How to use multiple validation rule sets per model in cakephp

  1. I know this is a response on an old post but…

    You know this rule isn’t a good solution?

    ‘firstname’ => array(‘rule’ => ‘/[A-Za-z ]+/’, ‘message’ => ‘Firstname must contain letters and spaces only.’),

    It wont allow input like ‘84165’ but it will allow this: ‘jelle842’.

Leave a Reply

Your email address will not be published. Required fields are marked *