Using Custom Validation Rules To Compare Two Form Fields

By Aran Johnson (aranworld)
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.

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 Form::error 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 580

CakePHP Team Comments Author Comments
 

Comment

1 Feedback

@Aran: thanks for sharing! Can you please update your article as Changeset 6187 has implemented the equalTo validation? Thanks!
Posted Dec 26, 2007 by Mariano Iglesias
 

Comment

2 Now updated to reflect more recent changes...

@Aran: thanks for sharing! Can you please update your article as Changeset 6187 has implemented the equalTo validation? Thanks!
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.
Posted Dec 31, 2007 by Aran Johnson
 

Comment

3 Modified in response to 1.2 beta

@Aran: thanks for sharing! Can you please update your article as Changeset 6187 has implemented the equalTo validation? Thanks!
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.

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.
Posted Jan 2, 2008 by Aran Johnson
 

Comment

4 new password and confirm password field populated with hash after validation fails

I have one small problem using this with Auth (with your instructions above).

When the validation fails, $this->data['User']['new_passwd'] and $this->data['User']['confirm_passwd'] is populated with the HASH.

Any ideas?
Posted Jan 4, 2008 by Baz L
 

Comment

5 Nullifying password values

I have one small problem using this with Auth (with your instructions above).

When the validation fails, $this->data['User']['new_passwd'] and $this->data['User']['confirm_passwd'] is populated with the HASH.

Any ideas?

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.
Posted Jan 7, 2008 by Aran Johnson
 

Question

6 convertPasswords hashes before valdiation. How do I prevent this


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.

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()?
Posted Jan 7, 2008 by Baz L
 

Comment

7 Calling AuthComponent in Model

Somehow calling the Auth comp. from the model's beforeSave()? 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
Posted Jan 9, 2008 by Michał Bachowski
 

Comment

8 No Loop

Isn't there supposed to be a loop with this array_shift thing?

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);
Posted Jan 9, 2008 by Baz L
 

Comment

9 Loop is not necessary

Isn't there supposed to be a loop with this array_shift thing?
I don`t use Cake 1.2 (still 1.1.x ;) ), but Aran wrote in the article:
The call to this method happens from within Model::invalidFields(). When it is called, the first parameter is passed as an array:

array('email' => 'webmaster@gmail.com);
So IMO there`ll be only one item in array - foreach() isn`t necessary.

I'm pretty sure calling statically isn't going to work, since it's not defined that way. It will work both PHP 4 and 5 (but in PHP5 very slow).

I'll just extract the contents of it. It's a one liner anyways... If you use it only once it`s ok, but what if you want to use it many times in many places?

Regards
Posted Jan 9, 2008 by Michał Bachowski
 

Comment

10 foreach not needed

So IMO there`ll be only one item in array - foreach() isn`t necessary.
Regards

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.
Posted Jan 11, 2008 by Aran Johnson
 

Bug

11 Modification to convertPasswords()

I have modified the example convertPassword() function. In my original version, there was the possibility of a user creating an empty password, because when an empty value is hashed, it creates a full length hash that would be entered into the database.
Posted Jan 11, 2008 by Aran Johnson
 

Comment

12 simplify model method

you should simplify the method in your model... There's no need for it to be that complicated, something like:

function identicalFieldValues($field1, $field2) {
if (!strcmp($this->data['User'][key($field1)], $this->data['User'][$field2]))
return true;
return false;
}

should achieve the same result
Posted May 3, 2008 by nick milosevic
 

Comment

13 not so sure about strcmp

strcmp is returning false positives for me. Michal's alternate code using the '===' is a better abbreviated version of this.
Posted Jun 10, 2008 by Aran Johnson
 

Comment

14 Great Post

Aran, thanks for the great example using Cake's custom validation feature. I've added this to my application and did some cleanup of my own to comply with the documentation's convention. Cheers!
Posted Aug 23, 2008 by Oliver John Tibi
 

Comment

15 Alternative to convertPasswords

I'm new to cake, so I'd like to hear some thoughts on this. I'm using Auth, so my password is being hashed before I can validate it. But instead of using a 'new_passwd' and convertPasswords, it occurred to me that my confirmpassword field is not being hashed. So, I can do all of my complexity validation on the confirmpassword, then hash it for comparision to password.
I made a small change to identicalFieldValues:

  function identicalFieldValues( $field=array(), $compare_field=null )
  {
     foreach( $field as $key => $value ){
          $v1 = $value;
          $v2 = $this->data[$this->name][ $compare_field ];
          if ($key == 'password') $v2 = AuthComponent::password($v2);
          if($v1 !== $v2) {
              return FALSE;
          } else {
              continue;
          }
      }
      return TRUE;
  }
}

I've added the line to hash the compare field if I'm validating 'password'.
(This needs some work to compare against Auth->fields['password'] instead of assuming the field is named 'password', and to detect if Auth is even being used, but this works for me.)

Here's my $validate:

  var $validate = array(
    'password' => array(
        'rule' => array('identicalFieldValues', 'confirmpassword' ),
        'message' => 'Passwords do not match.'
    ),

    'confirmpassword' => array(
        'complex' => array(
            'rule' => array('custom', '/(?=^.{7,}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/'),
            'message' => 'Password should be at least 7 characters, must have at least 1 upper case, lower case and numeric or special character.'
        ),
        'notEmpty' => array(
            'rule' => array('notEmpty'),  
            'allowEmpty' => false,
            'message' => 'Please enter a password.'
        ),
    ),
  );

So far, the only problem I've seen is that the error messages get swapped from what would be expected; The 'Passwords do not match' message appears on the password field and the 'Please enter a password' appears on the confirmpassword field. So I just arranged my form so the confirmpassword error appears above the field, between the two.
Posted Oct 17, 2008 by Chris Darling