Improved Advance Validation with Parameters

By Ludge (Ludge)
This code allows you to perform complex validation on your Model data using both regular expressions and functions as well as supporting multiple validation routines per model field.
This is an article ported from the wiki. The original author is evansagge.

Introduction

The built-in functions for validating model data are fine for simple validation techniques, but quite often you will find yourself needed to perform several validations on the same field.

The method of validation below will allow you to perform complex validation of your data with almost no extra work beyond describing the validation.


AppModel Additions

To begin with, the following code should be added to your AppModel.

Model Class:

Download code <?php 
// file: /app/app_model.php
define('VALID_WORD''/^\\w+$/');
define('VALID_UNIQUE''isUnique');
define('VALID_LENGTH_WITHIN''isLengthWithin');
define('VALID_CONFIRMED''isConfirmed');
 
class 
AppModel extends Model {
 
  
// If you need to disable validation for particular columns, you may populate this variable like so:
  // $this->User->disabledValidate = array('email', 'password'); // disables validation on email and password columns
  // $this->User->disabledValidate = array(
  //   'email',
  //   'password' => array('confirmed', 'required')
  // ); // disables validation on email column, and password['confirmed'] && password['required']
  
var $disabledValidate;
  
  function 
loadValidation() {
    
// placeholder for overloading
  
}
     
  function 
invalidFields($data = array()) {
    
$this->loadValidation();

    if (!
$this->beforeValidate()) {
        return 
false;
    }

    if (
is_array($this->disabledValidate)) {
      foreach(
$this->disabledValidate as $field => $params) {
        if (
is_string($field) && is_array($params)) {
          foreach(
$params as $param) {
            if (
is_string($param)) {
              
$this->validate[$field][$param] = false;
            }
          }
        } else if (
is_int($field) && is_string($params)) {
          
$this->validate[$params] = false;
        }
      }
    }
    
    
//debug($this->validate);
    
    
if (!isset($this->validate) || !empty($this->validationErrors)) {
      if (!isset(
$this->validate)) {
        return 
true;
      } else {
        return 
$this->validationErrors;
      }
    }
 
    if (isset(
$this->data)) {
      
$data array_merge($data$this->data);
    }
 
    
$errors = array();
    
$this->set($data);
 
    foreach (
$data as $table => $field) {
      foreach (
$this->validate as $field_name => $validators) {
        if (
$validators) {      
          foreach(
$validators as $validator) {
            if (isset(
$validator['method'])) {
              if (
method_exists($this$validator['method'])) {
                
$parameters = (isset($validator['parameters'])) ? $validator['parameters'] : array();
                
$parameters['var'] = $field_name;
                if (isset(
$data[$table][$field_name]) &&
                  !
call_user_func_array(array(&$this$validator['method']),array($parameters))) {
                  if (!isset(
$errors[$field_name])) {
                    
$errors[$field_name] = isset($validator['message']) ? $validator['message'] : 1;
                  }
                }
              } else {
                if (isset(
$data[$table][$field_name]) &&
                  !
preg_match($validator['method'], $data[$table][$field_name])) {
                  if (!isset(
$errors[$field_name])) {
                    
$errors[$field_name] = isset($validator['message']) ? $validator['message'] : 1;
                  }
                }
              }
            }
          }
        }
      }
    }
    
$this->validationErrors $errors;
    return 
$errors;
  }
  
  
// validation methods
    
  
function isUnique($params) {
    
$val $this->data[$this->name][$params['var']];
    
$db $this->name '.' $params['var'];
    
$id $this->name '.id';
    if(
$this->id == null ) {
      return(!
$this->hasAny(array($db => $val ) ));
    } else {
      return(!
$this->hasAny(array($db => $val$id => '!='.$this->data[$this->name]['id'] ) ) );
    }
  }
 
  function 
isLengthWithin($params) {
    
$val $this->data[$this->name][$params['var']];
    
$length strlen($val);
 
    if (
array_key_exists('min'$params) && array_key_exists('max'$params)) {
      return 
$length >= $params['min'] && $length <= $params['max'];
    } else if (
array_key_exists('min'$params)) {
      return 
$length >= $params['min'];
    } else if (
array_key_exists('max'$params)) {
      return 
$length <= $params['max'];
    }
  }
 
  function 
isConfirmed($params) {
    
$val $this->data[$this->name][$params['var']];
    
$val_confirmation array_key_exists('confirm_var'$params) ?
      
$this->data[$this->name][$params['confirm_var']] :
      
$this->data[$this->name][$params['var'].'_confirmation'];
    return 
$val == $val_confirmation;
  }
}
?>

You will notice that all the validation routines are DEFINE()d before the class is declared. This helps keep the appearance of the validation system uniform and does not differentiate between regex and method calls in the constant names.

The loadValidation() method is declared as empty, as it will be overloaded by our individual model classes later. It will be called by the invalidFields() method.

If you need to disable validation on certain columns or column validation routines, you can populate the disabledValidate array on the controller before calling save().


Usage

An example of how to use these complex validation routines is shown below, using the example of a Users model. Simply create a nested array containing the validation methods (and their parameters) to apply multiple validators to each field. A message can be defined which may be displayed to the user on triggering the error.

Model Class:

Download code <?php 
class User extends AppModel {
  var 
$name 'User';
  var 
$validate;
 
  function 
loadValidation(){
    
$this->validate = array(
      
'username' => array(
        
'required' => array(
          
'method' => VALID_NOT_EMPTY,
          
'message' => 'You have not entered a username.',
        ),
        
'word' => array(
          
'method' => VALID_WORD,
          
'message' => 'The username you entered contains invalid characters.'
        
),          
        
'unique' => array(
          
'method' => VALID_UNIQUE,
          
'message' => 'The username you entered is already in use.'
        
),
        
'length_within' => array(
          
'method' => VALID_LENGTH_WITHIN,
          
'message' => 'Username should be between 6 to 50 characters long.',
          
'parameters' => array('min' => 6'max' => 50)
        ),   
      ),
      
'email' => array(
        
'required' => array(
          
'method' => VALID_NOT_EMPTY,
          
'message' => 'You have not entered an e-mail address.',
        ),
        
'email' => array(
          
'method' => VALID_EMAIL,
          
'message' => 'The e-mail address you entered is not in proper format.'
        
),          
        
'unique' => array(
          
'method' => VALID_UNIQUE,
          
'message' => 'The e-mail address you entered is already in use.'
        
),
        
'confirmed' => array(
          
'method' => VALID_CONFIRMED,
          
'message' => 'The e-mail addresses you entered does not match its confirmation.'
        
),
      ),
      
'password' => array(
        
'required' => array(
          
'method' => VALID_NOT_EMPTY,
          
'message' => 'You have not entered a password.',
        ),
        
'length_within' => array(
          
'method' => VALID_LENGTH_WITHIN,
          
'message' => 'Password should be between 8 to 50 characters long.',
          
'parameters' => array('min' => 8'max' => 50)
        ),           
        
'confirmed' => array(
          
'method' => VALID_CONFIRMED,
          
'message' => 'The password you entered does not match its confirmation.'
        
),
      ),               
    );
  }
}
?>
When using VALID_LENGTH_WITHIN, you can either specify min parameter (only validates minimum length requirement), max parameter (only validates maximum length requirement), or both.



When using these routines, there is no difference as far as the controller is concerned:

Controller Class:

Download code <?php 
class UsersController extends AppController {
  var 
$name 'Users';
 
  var 
$helpers = array('Html''Error''Javascript''Ajax');
 
  function 
register() {
    if (!empty(
$this->data)) {
      if (
$this->User->save($this->data)) {
        
$this->flash('You have successfully registered your account.''/users');
      }
    }
  }
}
?>



Displaying errors

The following error helper can be used from a view to display the error messages defined within the model.

Helper Class:

Download code <?php 
class ErrorHelper extends Helper {
 
  function 
messageFor($target) {
      list(
$model$field) = explode('/'$target);
 
      if (isset(
$this->validationErrors[$model][$field])) {
          return 
'<span class="form_error_message">'.$this->validationErrors[$model][$field].'</span>';
      } else {
          return 
null;
      }
  }
  
  function 
allMessagesFor($model) {
    
$html =& new HtmlHelper;
    
    if (isset(
$this->validationErrors[$model])) {
      
$list '';
      foreach (
array_keys($this->validationErrors[$model]) as $field) {
        
$list .= $html->contentTag('li'$this->validationErrors[$model][$field]);
      }
      return 
$html->contentTag('div'
        
$html->contentTag('h4''The following errors need to be corrected: ') . 
        
$html->contentTag('ul'$list), array('class'=>'error_messages'));
    }
  }
}
?>

And finally, an example of the helper as used from the view:

View Template:

Download code
<?php echo $error->allMessagesFor('User'); // This line is for displaying the error messages from our form all at once. ?>
 
<form id="register" name="register" method="POST" action="<?php echo $html->url('/users/register')?>">
  <label>Username</label>
  <?php echo $html->input('User/username')?>
  <?php echo $error->messageFor('User/username')?>
  <br />
 
  <label>Email</label>
  <?php echo $html->input('User/email')?>
  <?php echo $error->messageFor('User/email')?>
  <br />
 
  <label>Confirm Email</label>
  <?php echo $html->input('User/email_confirmation')?>
  <?php echo $error->messageFor('User/email_confirmation')?>
  <br />
 
  <label>Password</label>
  <?php echo $html->password('User/password')?>
  <?php echo $error->messageFor('User/password')?>
  <br />
 
  <label>Confirm Password</label>
  <?php echo $html->password('User/password_confirmation')?>
  <?php echo $error->messageFor('User/password_confirmation')?>
  <br />
 
  <?php echo $html->submit('Register')?>
</form>

Note:

Don't forget that you can add your own validation routines by adding a regular expression/method to the AppController class.

 

Comments 55

CakePHP Team Comments Author Comments
 

Question

1 Invalidate field from controller

Using this method of validation, how can someone invalidate a field from the controller. If I use the native code in cake to validate, I can typically call Model->invalidate('field') to invalidate. It would be helpful to make a similar call where you could invalidate in the controller and specifiy the message (ie. Model->invalidateField('username','We do not like your username, change it') )

Anybody have insight on how I could accomplish this?
Posted Oct 4, 2006 by Ben Bush
 

Bug

2 contentTag deprecated

html->contentTag is deprecated with note: "This seems useless. Version 0.9.2"

a workaround is to do this in the errorHelper

{{{
class ErrorHelper extends Helper {
private $html;

function messageFor($target) {
list($model, $field) = explode('/', $target);

if (isset($this->validationErrors[$model][$field])) {
return ''.$this->validationErrors[$model][$field].'';
} else {
return null;
}
}

function allMessagesFor($model) {
$html =& new HtmlHelper;
$this->html = $html;

if (isset($this->validationErrors[$model])) {
$list = '';
foreach (array_keys($this->validationErrors[$model]) as $field) {
$list .= $this->contentTag('li', $this->validationErrors[$model][$field]);
}
return $this->contentTag('div',
$this->contentTag('h4', 'Der opstod følgende fejl: ') .
$this->contentTag('ul', $list), array('class'=>'error_messages'));
}
}

function contentTag($name, $content, $options = null) {
return "<$name " . $this->html->parseHtmlOptions($options) . ">$content";
}
}
?>
}}}
Posted Oct 8, 2006 by Christian Winther
 

Comment

3 Validating Non Strings

Excellent job; very useful code.

I made a few small adjustments to allow it to validate non-strings.

I changed line 64

from:
if (isset($data[$table][$field_name]) &&

to:
if ( array_key_exists($field_name, $data[$table]) &&

so a null field value then may be (in)validated too. I also changed line 71

from:
if (isset($data[$table][$field_name]) &&

to:
if (!array_key_exists($field_name, $data[$table]) || !is_string( $data[$table][$field_name]) ||

I also threw the following check into the top of any validation functions expecting a string:
if (!is_string($val)) {
return false;
}

There might be something wrong with my code, but it works so far (in concert with isType/nonNull validation funcs I added). Also, you may want nulls to go unvalidated, but the controller should be able to handle that logic by popping null fields into the disabledValidate array.

Posted Oct 12, 2006 by Nate Kidwell
 

Comment

4 How to manually invalidate a field

In response to Ben Bush's question, here's one method to invalidate a field in the controller.

First add this code to your app_model.php file:

function invalidate($field, $message) {
if (!is_array($this->validationErrors)) {
$this->validationErrors = array();
}
$this->validationErrors[$field] = $message;
}

Then in your controller call it as so:
$this->ModelName->invalidate('fieldname', 'error message');

it works for me...
Posted Oct 13, 2006 by Giles Paterson
 

Comment

5 More general way to add a custom invalidation

In response to Ben Bush's question and Giles answer about a separate invalidation function.
Giles code works, but unfortunately if you're invalidating a field before the save, only that error will be displayed and all the other checks wont run anymore.
If you want to have all errors returned, not only the custom one, use this:

replace

if (!isset($this->validate) || !empty($this->validationErrors)) {
if (!isset($this->validate)) {
return true;
} else {
return $this->validationErrors;
}
}

by

if (!isset($this->validate)) {
return true;
}



replace

$this->validationErrors = $errors;
return $errors;
}

by

if (!empty($this->validationErrors))
$errors += $this->validationErrors;

$this->validationErrors = $errors;
return $errors;
}

then you can add this function:

function invalidateField($fieldname, $error) {
if (empty($this->validationErrors) || !is_array($this->validationErrors))
$this->validationErrors = array($fieldname => $error);
else
$this->validationErrors[$fieldname] = $error;
}

use like this from controller:
$this->Model->invalidateField('username', 'I dont like your username');
Posted Oct 13, 2006 by Matt Keller
 

Bug

6 beforeValidate() isnt called

invalidFields is responsible to call beforeValidate() which this version doesn't

Add this just after the $this->loadValidation() :

if (!$this->beforeValidate()) {
return false;
}

Matt
Posted Oct 15, 2006 by Matt Keller
 

Comment

7 This needs to be updated with multiple form validation

This article only allows for validation of a single model. You may have a controller with multiple template views and different forms.

I added a switch case where I can pass a variable into the validation object and the script takes appropriate validation depending on the controller method I'm using.
Posted Oct 19, 2006 by Giovanni Glass
 

Comment

8 Uploaded file validation method

isUploadedFile validation method:
From the code in PEAR::Quickform

function isUploadedFile($params){
        $val = $this->data[$this->name][$params['var']];

        if ((isset($val['error']) && $val['error'] == 0) ||
            (!empty($val['tmp_name']) && $val['tmp_name'] != 'none')) {
            return is_uploaded_file($val['tmp_name']);
        } else {
            return false;
        }
    }
Posted Oct 27, 2006 by Spout
 

Question

9 Validating multiple models in a single form

Can someone post an example of how you could validate multiple models in a single form view, while not altering the cake core risking breakage during future core updates. Thanks.
Posted Mar 21, 2007 by jh
 

Question

10 cool

had some issues with it at first, but works ok now. thanks for sharing.
Posted Apr 9, 2007 by Tom Maiaroto
 

Question

11 Validating multiple models in a single form

This article only allows for validation of a single model. You may have a controller with multiple template views and different forms.

I added a switch case where I can pass a variable into the validation object and the script takes appropriate validation depending on the controller method I'm using.

i am trying to save entries in multiple table but i didn't find anyway to do thing using this example in my controller before save. can some explain how can i validate multiple model save in single form.

this is sample code i am using:
function register()
{
if (!empty($this->data))
{
$this->cleanUpFields();
$err2=$this->Org->invalidFields($this->data);
$err1=$this->Contact->invalidFields($this->data);
if(!is_array($err1) && !is_array($err2))
{
$this->Org->save($this->data, array('name'));
$this->Contact->save($this->data, array('firstname', 'lastname', 'birthday'));
}
}
}

but not working it showing all errors.
Posted Apr 18, 2007 by Rajesh
 

Comment

12 Multiple Model Validation

Thanks for the code. Very useful. I noticed however that you are passing posted data, which might contain data of more than one model, to invalidFields() function. And the problem is if multiple models are posted the function loops through all the models, thus error-checking all posted models.


Now imagine you post User data with some Comment data like so

$this->User->invalidFields($this->data)



User object will error-check both User and Comment. Ideally, basing on OOP basics, User has to check its own data and Comment has to check its own data like so

$this->User->invalidFields($this->data)
$this->Comment->invalidFields($this->data)


Therefore, i made some small changes.
insert at line 56 the following

$data = $data[$this->name];

comment out at line 60 the following
foreach ($data as $table => $field) {

comment out bracket on line 86
} (which belongs to above for-loop)

change line 68 to
if (isset($data[$field_name]) &&

change line 75-76 to

if (isset($data[$field_name]) && !preg_match($validator['method'], $data[$field_name])) {


The above will probably help clear things out to the post
posted Wed, Apr 18th 2007, 01:36 by Rajesh

Posted Jan 18, 2008 by Aziz
 

Comment

13 I already used this code

I already used this code to perform complex validation on my Model data. I used both regular expressions and functions as well as supporting multiple validation routines per model field. Works well.
Posted Jan 23, 2008 by Nat