Multiple rules of validation per field in CakePHP 1.2
There are many great improvements coming with CakePHP 1.2. On this article we'll take a look at multiple rules of validation per field, and how easy it is to use them on our 1.2 models.
On its 1.1 release, CakePHP only allowed us to define one rule of validation per field. If we needed to specify more than one rule, we either had to use some handy extensions to CakePHP that are available, or achieve multiple validation by using the callback method beforeValidate() in our models. CakePHP 1.2 brings us an improved technique to specify multiple rules per field, and best of all, it's backwards compatible, so our existing rules will continue to work.
What better way to start that with an example, specifically the typical Article CRUD example. We start by creating a simple controller that provide us with an add form:
and its matching articles/add.ctp view:
Let's take the classic Article model, and add only one validation to some fields:
Nothing new here, except that as you can see we're not defining the error messages. We are going to do so in the view. So we go back and change the add.ctp view so it now looks like this:
If we try the add action and submit the form with both fields empty, we'll get our newly defined error messages. What if we also wanted to add another rule of validation for the title field, where we should check that the title is at the most 100 characters long? Let's review the model, and now change it to look like this:
We have just converted the 'title' field to consist of an array of rules. The first rule is a CakePHP provided regular expression that checks for a specified value, and the second rule is a method existing in CakePHP's built in Validation class called 'maxLength'. Since this method takes an extra parameter (the number of maximum characters the field can contain), we add it on an array.
So we have now two conditions that can generate an error for the field title: an empty value, or a value with more than 100 characters. How do we differentiate the error messages on the view? Let's change the view so it now looks like:
As you can see we're setting an error message per rule. 0 corresponds to the first rule, 1 to the second, and so on. If we wanted more flexibility (such as having the option to change the order of the rules and still have the same error message assignment) and needed more readability, we can then use the string index approach. Change the model so it now looks like:
and change the view so it now looks like:
What about custom validation? What if we needed more rules than those provided by CakePHP's Validation class? Don't sweat, it comes very easy! All you need to do is set up your own validation functions on either your model or your AppModel class (if you wish to share them across your models.) For example, we're going to add a new validation rule to allow us to specify a minimum and a maximum length for our title. I know, what's the point when we have both minLength and maxLength in CakePHP's Validation class? Well, to show how it can be done :)
Edit the model and change it so it now looks like this:
A custom validation function takes one mandatory first parameter: the value to validate, and must return a boolean value of true when the value validates, or false when it doesn't. Extra parameters will be sent to the validation function as an array through its second parameter, and the values in the array are those values specified in the validation rule that do not correspond to CakePHP's internal values (such as rule or allowEmpty.)
What better way to start that with an example, specifically the typical Article CRUD example. We start by creating a simple controller that provide us with an add form:
Controller Class:
Download code
<?php
class ArticlesController extends AppController {
var $name = 'Articles';
var $helpers = array('Form');
function add() {
if (!empty($this->data)) {
// We don't do any real saving, we just validate the model
if ($this->Article->create($this->data) && $this->Article->validates()) {
$this->set('valid', true);
}
}
}
}
?>
and its matching articles/add.ctp view:
View Template:
Download code
<?php echo $form->create('Article'); ?>
<?php echo $form->input('title'); ?>
<?php echo $form->input('body'); ?>
<?php echo $form->end('Add Article'); ?>
Let's take the classic Article model, and add only one validation to some fields:
Model Class:
Download code
<?php
class Article extends AppModel {
var $name = 'Article';
var $validate = array(
'title' => VALID_NOT_EMPTY,
'body' => VALID_NOT_EMPTY
);
}
?>
Nothing new here, except that as you can see we're not defining the error messages. We are going to do so in the view. So we go back and change the add.ctp view so it now looks like this:
View Template:
Download code
<?php echo $form->create('Article'); ?>
<?php echo $form->input('title', array('error' => 'Please specify a valid title')); ?>
<?php echo $form->input('body', array('error' => 'Please specify a valid body')); ?>
<?php echo $form->end('Add Article'); ?>
If we try the add action and submit the form with both fields empty, we'll get our newly defined error messages. What if we also wanted to add another rule of validation for the title field, where we should check that the title is at the most 100 characters long? Let's review the model, and now change it to look like this:
Model Class:
Download code
<?php
class Article extends AppModel {
var $name = 'Article';
var $validate = array(
'title' => array(
VALID_NOT_EMPTY,
array(
'rule' => array('maxLength', 100)
)
),
'body' => VALID_NOT_EMPTY
);
}
?>
We have just converted the 'title' field to consist of an array of rules. The first rule is a CakePHP provided regular expression that checks for a specified value, and the second rule is a method existing in CakePHP's built in Validation class called 'maxLength'. Since this method takes an extra parameter (the number of maximum characters the field can contain), we add it on an array.
So we have now two conditions that can generate an error for the field title: an empty value, or a value with more than 100 characters. How do we differentiate the error messages on the view? Let's change the view so it now looks like:
View Template:
Download code
<?php echo $form->create('Article'); ?>
<?php echo $form->input('title', array('error' => array(
0 => 'Please specify a valid title',
1 => 'The title must have no more than 100 characters'
))); ?>
<?php echo $form->input('body', array('error' => 'Please specify a valid body')); ?>
<?php echo $form->end('Add Article'); ?>
As you can see we're setting an error message per rule. 0 corresponds to the first rule, 1 to the second, and so on. If we wanted more flexibility (such as having the option to change the order of the rules and still have the same error message assignment) and needed more readability, we can then use the string index approach. Change the model so it now looks like:
Model Class:
Download code
<?php
class Article extends AppModel {
var $name = 'Article';
var $validate = array(
'title' => array(
'required' => VALID_NOT_EMPTY,
'length' => array( 'rule' => array('maxLength', 100) )
),
'body' => VALID_NOT_EMPTY
);
}
?>
and change the view so it now looks like:
View Template:
Download code
<?php echo $form->create('Article'); ?>
<?php echo $form->input('title', array('error' => array(
'required' => 'Please specify a valid title',
'length' => 'The title must have no more than 100 characters'
))); ?>
<?php echo $form->input('body', array('error' => 'Please specify a valid body')); ?>
<?php echo $form->end('Add Article'); ?>
Custom Validation
What about custom validation? What if we needed more rules than those provided by CakePHP's Validation class? Don't sweat, it comes very easy! All you need to do is set up your own validation functions on either your model or your AppModel class (if you wish to share them across your models.) For example, we're going to add a new validation rule to allow us to specify a minimum and a maximum length for our title. I know, what's the point when we have both minLength and maxLength in CakePHP's Validation class? Well, to show how it can be done :)
Edit the model and change it so it now looks like this:
Model Class:
Download code
<?php
class Article extends AppModel {
var $name = 'Article';
var $validate = array(
'title' => array(
'required' => VALID_NOT_EMPTY,
'length' => array( 'rule' => 'validateLength', 'min' => 5, 'max' => 100 )
),
'body' => VALID_NOT_EMPTY
);
function validateLength($value, $params = array()) {
$valid = false;
$params = am(array(
'min' => null,
'max' => null,
), $params);
if (empty($params['min']) || empty($params['max'])) {
$valid = false;
} else if (strlen($value) >= $params['min'] && strlen($value) <= $params['max']) {
$valid = true;
}
return $valid;
}
}
?>
A custom validation function takes one mandatory first parameter: the value to validate, and must return a boolean value of true when the value validates, or false when it doesn't. Extra parameters will be sent to the validation function as an array through its second parameter, and the values in the array are those values specified in the validation rule that do not correspond to CakePHP's internal values (such as rule or allowEmpty.)
Comments
Question
1 Problem with multiple validation criteria
username, password, email, first_name, last_name.
I've tried the example and $form->input('username', 'error' => 'error msg') works if I put only one validation rule.
But as I've tried:
var $validate = array(
'username' => array(
VALID_NOT_EMPTY,
array(
'rule' => array('maxLength', 40)
)
),
'password' => VALID_NOT_EMPTY,
'email' => VALID_EMAIL
);
Controler code:
function register()
{
if (!empty($this->data))
{
if ($this->User->create($this->data) && $this->User->validates()) {
$this->set('valid', true);
}
}
}
And view:
<?php echo $form->input('username',
array( 'error' => array(
0 => 'Username field must be filled',
1 => 'The title must have no more than 40 characters',
))); ?>
If I submit a form with no username value there is no error msg. If I input any value into it I get 'Array'.
Could anybody tell me what am I doing wrong in this one?
Comment
2 Problem with multiple validation criteria
Comment
3 Thank You
Comment
4 Order Matters
var $validate = array (
'email' => array (
'valid' => array(
'rule' => array('email')
),
'required' => array (
'rule' => array("minLength", 1)
)
)
);
works ok, but
var $validate = array (
'email' => array (
'required' => array (
'rule' => array("minLength", 1)
),
'valid' => array(
'rule' => array('email')
)
)
);
Generates 'valid' when left blank and when something different from an email. I'm not sure if this is the same behavior on cake 1.1
Comment
5 Error string in the validation rule
So, the validation array simply becomes something like
var $validate = array( 'column_name' =>
array('ruleIndexName' => 'numeric',
'message' =>'*Valid characters: numbers only please'));
That way, in all your forms, you do not need to specify the error message, as the message in the Model's validation array is passed to the form and displayed.
Comment
6 Error string in the validation rule
The possibility of declaring the error string in the model itself instead of repeating it on the views is really attractive, could you please elaborate on how to use this?
Comment
7 Custom Validation Function
Is it possible to get the field-name in a custom vaildation function without giving it as a param in the 'rule'-array?
Bye,
Alex
Comment
8 In depth look at defining the error string in the validation array
Sure, I will describe how I am using it. (I will copy/paste my actual code - so this is working code - for me at least).
I have a 'User' model which queries my 'users' mysql table. I use the method described above for a couple of things when interacting with the 'users' table.
validating data for:
I started looking around for an easier way so that I wouldn't have to copy and paste error strings all over the place in my views. It became a hassle when I wanted to change the wording in the "error message", I had to remember which views where were responsible for handing out this message, compound that with all the validations in all the other sections of my web apps, and it quickly became a "finding a needle in a haystack" situation - i thought, there must be a better way. And there was/is :)
I found this out by reviewing/studying the model/controller/validation/form sections of the 1.2 API - http://api.cakephp.org/ is my best friend these days :)
First, as described above, add the error string value to the 'message' attribute when defining the validation array. An example of my validation array that I use for the 'User' model is:
<?php
var $validate = array(
'username' => array( 'rule' => 'alphaNumeric', 'message' =>'Valid characters: letters and numbers only'),
'firstname' => array( 'rule' => 'alphaNumeric', 'message' =>'Valid characters: letters and numbers only'),
'lastname' => array( 'rule' => 'alphaNumeric', 'message' =>'Valid characters: letters and numbers only'),
'displayname' => array( 'rule' => array('custom', '/^[\da-zA-Z _]+$/'), 'message' =>'Valid characters: letters,numbers,spaces,underscores only'),
'password' => array( 'rule' => array('custom', '/^.{6,}$/'), 'message' =>'Passwords must be at least 6 characters in length'),
'email' => array( 'rule' => 'email', 'message' =>'E-mail must be a valid e-mail address')
);
?>
This centralizes the error strings in one place, I can change them to whatever I want until my heart is content, and they automatically update in all my views.
The controller code is very basic, and if I take my logon page as an example, all it does is verify that the user is not already logged in, manually validate the data to see if the entered data is OK (validates using the validation array in the model), If the data entered validates, see if the username/password entered are good, and do the appropriate action. (Note, I use a custom component that I called SiteAuth, which actually handles the authentication check, among other things, like figure out the users history on my site, and redirect the user to the last page that he was visiting (the $this->SiteAuth->getRegexURL('/users/login') line). Also ignore the $this->menu stuff, again, a custom component. It's just to set my menu entries for the site):
<?php
function login() {
//only allow access to the page if user has not logged in before
if ( $this->Session->read('userid') != 1 ) {
$this->Session->setFlash('You are already logged in. <br />You do not need to access the logon page again.');
$this->redirect('/');
exit;
}
//set the view error var to false. Used to display bad user/pass error message
$this->set('error', false);
//if we have data
if( ! empty($this->data) ) {
//pass the data to the model so we can manually validate things
$this->User->data = $this->data;
//validate the data, and try to authenticate the user
if ( $this->User->validates() ) {
if ( $this->SiteAuth->login($this->data['User']['username'], $this->data['User']['password']) ) {
$this->Session->setFlash('You have been logged in successfully.');
$this->redirect($this->SiteAuth->getRegexURL('/users/login'));
exit;
} else {
$this->set('error', true);
}
}
}
//set the page menu options
$this->Menu->setMenu('botMenu');
}
?>
The main point here is the manual validation method. We first pass the form data the user entered to the Model:
$this->User->data = $this->data;
Next, we manually see if the data validates:
if ( $this->User->validates() ) {}
Once we do this, if the data entered does not validate, the error messages in the model's validation array are populated.
As far as the view is concerned, again, it is very basic. Make sure you use the form helper in your controller:
var $helpers = array('Html', 'Form');
Then in your view, just use the form helper for displaying your form fields:
<div id="loginbox">
<div id="loginHeader">
<span>Login</span>
</div>
<div id="loginBody">
<?php if ($error){?>
<p class="error">The login credentials you supplied could not be recognized. Please try again.</p>
<?php } ?>
<?php echo $form->create('User', array('action' => 'login'));?>
<table border="0">
<tr>
<td class="right">
<?php echo $form->label('username', 'Username: ');?>
</td>
<td>
<?php echo $form->input('User.username', array('type'=>'text', 'label'=>false, 'div'=>false));?>
</td>
</tr>
<tr>
<td class="right">
<?php echo $form->label('password');?>
</td>
<td>
<?php echo $form->input('User.password', array('type'=>'password', 'label'=>false, 'div'=>false));?>
</td>
</tr>
</table>
<?php echo $form->submit('Login', array('class'=>'submit'));?>
<?php echo $form->end(); ?>
</div>
</div>
The form helper will automatically look for and display the error string in the 'message' attribute of the Model's validation array for any form input/checkbox/select... item that has an entry in it - that's it.
I use this all over the place - for everything basically. It also works if you don't manually validate things (like during a save operation, when the model automatically validates things before the save), so again, no magic in the view has to be done. The main thing is to make sure that the validation is done in the controller, so that those 'message' attributes will be populated so that the form helper can see them in the views, and display them for you.
I hope this makes more sense/helps some - let me know if I need to describe anything further.
Comment
9 Great explanation
The is some invaluable info, centralizing all validation error messages on the model can save hours of tedious work and hundreds of typos. In combination with i18n and l10n functionality this definitely rocks!
Question
10 Validating datetime
Should validations that check if a record exists on the database be on the model also? Like on sign up, the username is checked if it's unique. If so, how can I do it on the model?
Also, how can I validate data from $form->datetime fields? I can't seem to make the 'date' rule work. Should I use cleanUpFields() before validation or is it totally unrelated? =)
Comment
11 Another little trick.
My code gets a bit flattened and unreadable here.
public $validate = array(
'username' => array(
'short' => array(
'rule' => array('minLength',3),
'message' => 'At least 3 chars'),
'long' => array(
'rule' => array('maxLength',30),
'message' => 'Max 30 chars')
));
A validation rule like that will output one of the two messages even if you haven't specified their names in the view. Their names is irrelevant here. Plz. name them banana and Tarzan if you like.
Comment
12 Create function
Comment
13 About defining error messages in the model
The error itself is semantic, and so a well chosen string index is definitely advisable - but if one were to define all their messages in the model - they would lose flexibility in the views because a certain amount of presentation is dictated by the model directly.
It sounds like I'm splitting hairs - but Imagine that your site becomes suddenly successful - and you need to have pages in multiple languages - Then its easy to use routing and multiple views to add flexibility to your site -- so long as everything was defined in the views - otherwise your going to have to start re-writing models and/or controllers.
And if that is too much work - or results in to much hunting around - you can always use layouts, constants, resource files - etc to consolidate your error messages somewhere in the proper domain.
Proper MVC shouldn't only make _development_ fast and straight forward, but _maintenance_ as well.
BUT... defining those messages in the models certainly makes things easier on a lot of small to medium sites - and nothing (including MVC) is a golden hammer - so use your judgment.
Comment
14 Very good
Comment
15 validateUnique method
Model Class:
<?php
class AppModel extends Model {
function validateUnique($value, $params = array()) {
if (!empty($this->id)) {
$conditions = array($this->primaryKey => '!= '.$this->id, $params['field'] => $value);
} else {
$conditions = array($params['field'] => $value);
}
return !$this->field($this->primaryKey, $conditions);
}
}
?>
And then use it like so:
Model Class:
<?php
class User extends AppModel() {
var $name = 'User';
var $validate = array(
'email' => array(
array(
'rule' => 'validateUnique',
'field' => 'email',
'message' => 'This email address is already in use'
)
)
);
}
?>
Please note you have to specify the field name again in the parameters, or until the following enhancement ticket is implemented.
https://trac.cakephp.org/ticket/2766
Question
16 Variables in error messages
I'm building an app where one field must be in the form validTableName.validFieldName.
I using a custom validation method that in turn uses 3 validation rules, and I want a specific error message for any rule that fails...
In the MyModel::$validate looks like this...
var $validate = array(
"table_field" => array(
"rule" => "validateTableField",
"message" => "This is not what I want"
)
);
And here is the custom validation function, MyModel::validateTableField()....
function validateTableField(){
$table_field = $this->data["OptionField"]["table_field"];
if (!Validation::custom($table_field, VALID_SQL_TABLE_FIELD)){
// Setting the message here doesn't work. 'This is not what I want' is displayed instead.
$this->validate["table_field"]["message"] = "This field must be of the form table.field";
return false;
}
else {
list($table, $field) = explode(".", $table_field);
if (!$this->tableExists($table)){
// Setting the message here doesn't work. 'This is not what I want' is displayed instead.
$this->validate["table_field"]["message"] = "This table '".$table."' does not exist.";
return false;
}
else if (!$this->fieldExists($table, $field){
// Setting the message here doesn't work. 'This is not what I want' is displayed instead.
$this->validate["table_field"]["message"] = "The field '".$field."' is not a part of the '".$table."' table";
return false;
}
}
return true;
}
... and note that:
1) AppModel::tableExists($table) returns true only if $table is a valid table in the database
2) AppModel::fieldExists($table, $field) returns true only if $field is a valid field in $table
3) VALID_SQL_TABLE_FIELD is a constant regular expression that validates SQL table.field syntax.
thanks
-a-
Comment
17 Re Variables in error messages
Try this:
First, extend your AppModel like so:
Model Class:
<?php
// Setup a unique constant to ignore an error message
if (!defined('SILENT_ERROR_MESSAGE')) {
define('SILENT_ERROR_MESSAGE', '{D117A338-3DBB-4faa-8FEA-6024F2B5F41C}');
}
class AppModel extends Model {
function invalidate($field, $data = null) {
if ($data != SILENT_ERROR_MESSAGE) {
if (is_string($data) && strpos($data, '{{') !== false) {
$this->_currentInvalidFieldName = $field;
$data = preg_replace_callback(
'/\{\{(data|field)(?:\[(?:([-a-z0-9_]+)\.)?([-a-z0-9_]+)\])?\}\}/i',
array($this, '_formatErrorStringCallback'),
$data
);
$this->_currentInvalidFieldName = null;
}
return parent::invalidate($field, $data);
}
}
function _formatErrorStringCallback($bits) {
if ($bits[1] == 'field') {
if (empty($bits[2]) && empty($bits[3])) {
return Inflector::humanize($this->_currentInvalidFieldName);
}
}
else {
if (empty($bits[3])) {
return $this->data[$this->alias][$this->_currentInvalidFieldName];
}
else {
if (empty($bits[2])) {
return $this->data[$this->alias][$bits[3]];
}
else {
return $this->data[$bits[2]][$bits[3]];
}
}
}
// In case of invalid syntax, return the whole thing so
// the user sees the mistake he's made
return $bits[0];
}
}
?>
Now define your validation so that the 'message' is set to the constant, and the validation function invalidates the field manually:
Model Class:
<?php
class Product extends AppModel {
var $name = "Product";
var $belongsTo = array("Category");
var $validate = array(
'code' => array(
array(
'rule' => 'alphaNumeric',
'message' => '"{{data}}" is not a valid {{field}}',
'last' => true
),
array(
'rule' => 'uniqueCodeForCategory',
'message' => SILENT_ERROR_MESSAGE
)
)
);
function uniqueCodeForCategory($data) {
if (!$this->isUnique(array('code', 'category_id'), false)) {
$msg = 'The Product Code "{{data}}" is already allocated for ';
if ($catTitle = $this->Category->field($this->Category->displayField, array('Category.id' => $this->data['Product']['category_id']))) {
$msg .= 'the "'.$catTitle .'" category';
}
else {
$msg .= 'this category';
}
$this->invalidate('code', $msg);
return false;
}
return true;
}
}
?>
Works for me!
Question
18 multiple error messages
I have one further question:
How can I display multiple error-messages in my views. For example a "username" is only valid if it contains only [a-z0-9_-] and if the length is between 3 and 10 letters..., so i would use something like this:
Model Class:
<?php var $validate = array('name' => array(
'length' => array('rule'=> array('between', 5, 10), 'message'=>'Too short or too long'),
'valid' => array('rule'=>'#^[a-z0-9_-]*$#i', 'message'=>'Not valid...'),
));?>
Now it is possible, that validation fails for both rules: "$a" is too short and has a wrong sign. But with <?php echo $form->error('User.name');?> i only get one error-message.
Any hints?