Using Custom Validation Rules To Compare Two Form Fields
Want to make sure that two submitted form fields have the same value? Use the ability to write your own validation rules, to add this validation to your model.
It is very common when collecting a user's email address, or password, to force the user to type the string twice in two separate form fields. Prior to saving the data, one can check that the string in both fields match.
While there are already a number of ways to do this using Javascript alone, or Ajax, I wanted to use Cakephp 1.2's validation system to handle this task.
There doesn't exist a built-in validation that works quite right, but luckily you can write your own validation rules ( http://tempdocs.cakephp.org/#TOC132710 ) and they will process along with any built-in rules you are using.
I will demonstrate this using a simple contact information form.
Download code
Just a very basic add method. Note, however, that nothing in this controller deals with comparing form fields.
If you are new to Cake 1.2, this might look a bit strange. The html helper is no longer used for forms, but instead the 'form' helper is used.
Note that in the calls to the <b>Form::error</b> method, I do not include an error message. This message will be provided by the particular rule in the Contact class.
This is where things get interesting. Here is more information about Cake 1.2's new validation configuration: http://tempdocs.cakephp.org/#TOC127334.
The validate attribute contains an array. In the array, we declare that for the field email, we will use a rule called identicalFieldValues.
Download code
This line says that the rule will use the validation method identicalFieldValues, and when it calls this method it will provide as the second argument the string 'confirm_email'.
The call to this method happens from within Model::invalidFields(). When it is called, the first parameter is passed as an array:
Download code
The key is the string representing the field's name, and the value represents the value of that field. This is how all customized validation functions are now called.
The second argument is the string provided in the array under 'rule' in the validate attribute. In this case it is the string 'confirm_email'. This string represents the name of the field I am comparing the first field to.
To get the first value I extract it from the passed array.
The second value I extract from the Model's data array by using the string passed as the second argument.
Once I have the two variables set, I can compare them however I want. I return a false if the values don't match, and a true if they do.
Now, if a person submits the Contact form with mismatched values in the two email fields, the Contact::save method will fail and the form will be re-displayed with an error message.
If you are using the AuthComponent, and the name of the password field you are checking is equal to the column name for the User's password then this value will automatically be hashed prior to validation, but the 'confirm_password' value will NOT be hashed.
A fix to this, is to name the password fields in your Users/add form something like 'new_password' and 'confirm_password'. Before calling the User::save() method, hash both of these values using the Auth->password() function.
Download code
Then in a custom User::beforeSave() method, which is called after validation succeeds, pass the value of new_password to the data field for the real password field (most likely something like 'passwrd').
Download code
Using these modifications, you can now use the identicalFieldValues() function in your User model to make sure that when the user adds their requested password, that both fields match. In addition, don't forget that you can have multiple rules for each field ( http://tempdocs.cakephp.org/#TOC127334 ), so if you want to do any other checks on the password field you can do those as well.
While there are already a number of ways to do this using Javascript alone, or Ajax, I wanted to use Cakephp 1.2's validation system to handle this task.
There doesn't exist a built-in validation that works quite right, but luckily you can write your own validation rules ( http://tempdocs.cakephp.org/#TOC132710 ) and they will process along with any built-in rules you are using.
I will demonstrate this using a simple contact information form.
The Table
Download code
CREATE TABLE `contacts` (
`id` int(6) NOT NULL auto_increment,
`name` varchar(100) default NULL,
`email` varchar(200) default NULL,
PRIMARY KEY (`id`)
);
Controller Class:
Download code
<?php class ContactsController extends AppController {
var $name='Contacts';
var $uses = array('Contact');
var $helpers = array('form');
function add(){
if( !empty( $this->data ) ){
if( $this->Contact->save( $this->data ) ){
$lastId = $this->Contact->getLastInsertId();
$this->flash('Your new user has been created.','/c/contacts/view/'.$lastId );
}
}
}
}
?>
Just a very basic add method. Note, however, that nothing in this controller deals with comparing form fields.
View Template:
Download code
<h1>Enter Contact Information</h1>
<form method="post" action="<?php echo $html->url('/contacts/add')?>">
<?php echo $form->label('Contact.name', 'Full Name of Contact'); ?>
<?php echo $form->error('Contact.name'); ?>
<?php echo $form->text('Contact.name', array('size' => '80') ); ?>
<?php echo $form->label('Contact.email', "Contact's E-mail"); ?>
<?php echo $form->error('Contact.email'); ?>
<?php echo $form->text('Contact.email', array('size' => '80') ); ?>
<?php echo $form->label('Contact.confirm_email', 'Re-enter E-mail For Verification'); ?>
<?php echo $form->text('Contact.confirm_email', array('size' => '80') ); ?>
<?php echo $form->submit('Add Person to Directory'); ?>
</form>
If you are new to Cake 1.2, this might look a bit strange. The html helper is no longer used for forms, but instead the 'form' helper is used.
Note that in the calls to the <b>Form::error</b> method, I do not include an error message. This message will be provided by the particular rule in the Contact class.
Model Class:
Download code
<?php
class Contact extends AppModel
{
var $name = 'Contact';
var $validate = array(
'email' => array(
'identicalFieldValues' => array(
'rule' => array('identicalFieldValues', 'confirm_email' ),
'message' => 'Please re-enter your password twice so that the values match'
)
)
);
function identicalFieldValues( $field=array(), $compare_field=null )
{
foreach( $field as $key => $value ){
$v1 = $value;
$v2 = $this->data[$this->name][ $compare_field ];
if($v1 !== $v2) {
return FALSE;
} else {
continue;
}
}
return TRUE;
}
}
?>
This is where things get interesting. Here is more information about Cake 1.2's new validation configuration: http://tempdocs.cakephp.org/#TOC127334.
The validate attribute contains an array. In the array, we declare that for the field email, we will use a rule called identicalFieldValues.
Download code
'rule' => array('identicalFieldValues', 'confirm_email' )
This line says that the rule will use the validation method identicalFieldValues, and when it calls this method it will provide as the second argument the string 'confirm_email'.
The Home Brewed Validation Function
As the model code above illustrates, I added a method named identicalFieldValues into the Contact class.The call to this method happens from within Model::invalidFields(). When it is called, the first parameter is passed as an array:
Download code
array('email' => 'webmaster@gmail.com')
The key is the string representing the field's name, and the value represents the value of that field. This is how all customized validation functions are now called.
The second argument is the string provided in the array under 'rule' in the validate attribute. In this case it is the string 'confirm_email'. This string represents the name of the field I am comparing the first field to.
To get the first value I extract it from the passed array.
The second value I extract from the Model's data array by using the string passed as the second argument.
Once I have the two variables set, I can compare them however I want. I return a false if the values don't match, and a true if they do.
Now, if a person submits the Contact form with mismatched values in the two email fields, the Contact::save method will fail and the form will be re-displayed with an error message.
Using this for Passwords
The other obvious usage of this is when a new user registers and provides a password.If you are using the AuthComponent, and the name of the password field you are checking is equal to the column name for the User's password then this value will automatically be hashed prior to validation, but the 'confirm_password' value will NOT be hashed.
A fix to this, is to name the password fields in your Users/add form something like 'new_password' and 'confirm_password'. Before calling the User::save() method, hash both of these values using the Auth->password() function.
Download code
//add this function to the users_controller.php
function convertPasswords()
{
if(!empty( $this->data['User']['new_passwd'] ) ){
$this->data['User']['new_passwd'] = $this->Auth->password($this->data['User']['new_passwd'] );
}
if(!empty( $this->data['User']['confirm_passwd'] ) ){
$this->data['User']['confirm_passwd'] = $this->Auth->password( $this->data['User']['confirm_passwd'] );
}
}
Then in a custom User::beforeSave() method, which is called after validation succeeds, pass the value of new_password to the data field for the real password field (most likely something like 'passwrd').
Download code
//add this function to your user model and call it from within beforeSave()
function setNewPassword()
{
$this->data['User']['paswd'] = $this->data['User']['new_passwd'];
return TRUE;
}
function beforeSave(){
$this->setNewPassword();
return true;
}
Using these modifications, you can now use the identicalFieldValues() function in your User model to make sure that when the user adds their requested password, that both fields match. In addition, don't forget that you can have multiple rules for each field ( http://tempdocs.cakephp.org/#TOC127334 ), so if you want to do any other checks on the password field you can do those as well.
Comments
Comment
1 Feedback
Comment
2 Now updated to reflect more recent changes...
Thanks for the heads up. I have modified the article now to reflect the way the equalTo method has been implemented. I think the article is possibly more useful now, since the implemented equalTo() method doesn't seem to function in the way I write about in this article, so overwriting the core function makes more sense.
Comment
3 Modified in response to 1.2 beta
I have just seen the nice documentation in tempdocs about creating custom validation rules. So, I have eliminated all references to equalTo and instead focuesed this on being a tutorial about solving the compare fields problem by using custom validation rules.
Comment
4 new password and confirm password field populated with hash after validation fails
When the validation fails, $this->data['User']['new_passwd'] and $this->data['User']['confirm_passwd'] is populated with the HASH.
Any ideas?
Comment
5 Nullifying password values
In your Users::add() create the following logic:
if ($this->User->save($this->data)) {$this->flash('Your new user has been created.','/users/index' );
} else {
$this->data['User']['new_passwd'] = null;
$this->data['User']['confirm_passwd'] = null;
}
This will ensure that the hashed password is never re-displayed in the form.
Question
6 convertPasswords hashes before valdiation. How do I prevent this
I actually used unset() but I guess where you're going.
Here's another issue. When you call convertPasswords() it sends hashed values to the validation. This is fine for most people, but not for me. I can't do any other validation on the new_password fields (not empty, length, etc).
To answer my question this is what I've had to do: change all my $this->User->save's to:
// needed for validation for some reason
$this->User->set($this->data);
if ($this->validates($this->data))
{
$this->convertPasswords();
// all we did was hash passwords, no need to revalidate.
if ($this->save($this->data, false))
{
// do stuff
} else
{
// invalid
}
}
Is there a more elegant way to do this that doesn't break MVC guidelines? Somehow calling the Auth comp. from the model's beforeSave()?
Comment
7 Calling AuthComponent in Model
You can call password() statically (AuthComponent::password()), but you have to ensure that such class wis declared previously.
By the way:
Method identicalFieldValues() can by shortened:
function identicalFieldValues( $field=array(), $compareWith=null ) {
return($this->data[$this->name][$compareWith] === array_shift($field));
}
Regards
Comment
8 No Loop
I'm pretty sure calling statically isn't going to work, since it's not defined that way.
I'll just extract the contents of it. It's a one liner anyways:
return Security::hash(Configure::read('Security.salt') . $password);
Comment
9 Loop is not necessary
I don`t use Cake 1.2 (still 1.1.x ;) ), but Aran wrote in the article:
So IMO there`ll be only one item in array - foreach() isn`t necessary.
It will work both PHP 4 and 5 (but in PHP5 very slow).
If you use it only once it`s ok, but what if you want to use it many times in many places?
Regards
Comment
10 foreach not needed
You are probably right about the foreach() not being needed.
I mainly used it to make it very explicit that passed argument was an array.
Bug
11 Modification to convertPasswords()
Comment
12 simplify model method
function identicalFieldValues($field1, $field2) {
if (!strcmp($this->data['User'][key($field1)], $this->data['User'][$field2]))
return true;
return false;
}
should achieve the same result