Using Custom Validation Rules To Compare Two Form Fields

by 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

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:

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

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

<?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.

'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:
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.
//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').
//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.

Report

More on Tutorials

Advertising

Comments

  • aittam posted on 01/17/11 10:49:02 AM
    Custom validation method in model

        function equaltofield($check,$otherfield)
        {
            //get name of field
            $fname = '';
            foreach ($check as $key => $value){
                $fname = $key;
                break;
            }
            return $this->data[$this->name][$otherfield] === $this->data[$this->name][$fname];
        }

    in validation rules:


    in validation rules (password is the other field):
    [CODE] 'equaltofield' => array(
    'rule' => array('equaltofield','password'),
    'message' => 'Require the same value to password.',
    //'allowEmpty' => false,
    //'required' => false,
    //'last' => false, // Stop validation after this rule
    'on' => 'create', // Limit validation to 'create' or 'update' operations
    ),
  • daemonfire300 posted on 08/17/09 05:43:17 AM
    hi there,

    this question might be stupid, but i think there must be some newer version of "comparing 2 fields"?

    Additionally i think that the code used to create the view must also be corrected.
    As you can see in this tutorial the "form" helper syntax changed:
    http://book.cakephp.org/view/338/Data-Validation
    For more information about "form" helper use in 1.2 and higher releases:
    http://book.cakephp.org/view/182/Form
  • ojtibi posted on 08/23/08 02:51:01 PM
    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!
  • aranworld posted on 06/10/08 09:03:33 PM
    strcmp is returning false positives for me. Michal's alternate code using the '===' is a better abbreviated version of this.
  • niq000 posted on 05/03/08 02:20:55 PM
    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
  • aranworld posted on 01/11/08 12:49:38 PM
    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.
  • bazil749 posted on 01/04/08 07:38:13 AM
    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?
    • aranworld posted on 01/07/08 10:21:17 AM
      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.
      • bazil749 posted on 01/07/08 12:36:34 PM

        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()?
        • designysis posted on 10/17/08 06:49:12 AM
          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.
          • jhelm85 posted on 07/11/09 07:48:28 AM
            ...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.
            Genius. I've been looking all day for something like this. Thanks.
        • MiBek posted on 01/09/08 05:24:25 AM
          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
          • bazil749 posted on 01/09/08 06:19:36 AM
            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);
            • MiBek posted on 01/09/08 07:07:50 AM
              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
              • aranworld posted on 01/11/08 12:48:19 PM
                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.
  • mariano posted on 12/26/07 09:31:00 AM
    @Aran: thanks for sharing! Can you please update your article as Changeset 6187 has implemented the equalTo validation? Thanks!
    • aranworld posted on 12/31/07 11:33:10 AM
      @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.
      • aranworld posted on 01/02/08 12:22:56 PM
        @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.
login to post a comment.