Multiple Validation Sets in CakePHP 1.2

In CakePHP, you define how your data should be validated by setting parameters on the validate property of your model. In version 1.2, there is an on option that can be set on a specific rule that, when set, is either create or update. This allows you to define different rules depending on the type of action being performed. That, in combination with the required and allowEmpty properties, give you a fair amount of control over different validation rules.

Despite that, I developed a slightly different approach that allows for different validation sets to be specified and to be cleanly separated from each other.

I override the validates method within a custom AppModel (stored in /app/app_model.php). The validates method is called when a save call is made or it can be called manually. This custom method can perform in one of two ways:

Method One: Action-specific Validation Sets

It'll first look to see if you have a validation set specified for the current controller action. For example, if you were in the edit action, it'd look for a property in the model called validateEdit. If it doesn't exist, it'll default back to using the normal validate property.

class User extends AppModel {
   // performs normal validation
   var $validate = array( ... ); 
   // used in an edit action like /users/edit/1
   var $validateEdit = array( ... ); 
   // used in a forgotpassword action like /users/forgotpassword
   var $validateForgotpassword = array( ... );
}

In that Forgot Password example, this would allow you to avoid performing your own checks for even basic calls and keep the logic tucked away in the Model.

class UsersController extends AppController {
   function forgotpassword() {
      $this->User->set($this->data);
      if ($this->User->validates()) {
         // send email to reset password and show success message
      }
   }
}

The thing I like about this is that the error messaging is handled by the validation and the FormHelper. (Although, a more noticable flash message near the top of the page is also helpful.)

Method Two: Custom Validation Sets

Alternatively, you may wish to specify a validation set manually before calling save or validates on a model. To do this, just specify a validationSet property on the model right before your call. The property will be unset immediately afterwards allowing normal validation rules to be applied.

Here's an alternate approach to the forgotten password example:

class User extends AppModel {
   var $validateForgotpassword = array( ... );
}

class UsersController extends AppController {
   function forgot() {
      $this->User->set($this->data);
      $this->User->validationSet = 'forgotpassword';
      if ($this->User->validates()) {
         // send email to reset password and show success message
      }
   }
}

The Code

Here is the custom validates method that pulls this all off:

function validates($options = array()) {
    // copy the data over from a custom var, otherwise
    $actionSet = 'validate' . Inflector::camelize(Router::getParam('action'));
    if (isset($this->validationSet)) {
        $temp = $this->validate;
        $param = 'validate' . $validationSet;
        $this->validate = $this->{$param};
    } elseif (isset($this->{$actionSet})) {
        $temp = $this->validate;
        $param = $actionSet;
        $this->validate = $this->{$param};
    } 
    
    $errors = $this->invalidFields($options);

    // copy it back
    if (isset($temp)) {
        $this->validate = $temp;
        unset($this->validationSet);
    }
    
    if (is_array($errors)) {
        return count($errors) === 0;
    }
    return $errors;
}

Enjoy!

Published July 23, 2008
Categorized as CakePHP
Short URL: http://snook.ca/s/901

Conversation

18 Comments · RSS feed
Richard@Home said on July 23, 2008

That is a work of genius! Funnily enough, I was looking into this only yesterday... for forgotten password validation O.o

Bruno Bergher said on July 23, 2008

It's very nice solution Jonathan, something I may apply to my projects as well.

Perhaps it would also be worth considering using the 'on' key itself as an identifier for a validation set specific to an action. Maybe something like 'on' => 'actionEdit', or 'on'=>'actionForgotPassword' could do the trick, without straying too much off the normal model structure. I'd have to put some more though into this.

Anyway, my point here is to actually ask you a different thing (which could be slightly off-topic): we all end up creating this custom methods in app_model/app_controller, some new configuration settings, helper, components and lots of code we use on different projects, and end up becoming a custom framework.. I was wondering how do you (and your readers) manage this code to be included and kept in sync in different projects, along with the constant new versions of Cake itself.

Do you use external repositories, SVN templates, copy + paste? What do you think is a good method for keeping track of all of this code, and keeping it useful?

Jonathan Snook said on July 23, 2008

@Bruno: my largest complaint about the 'on' approach is that it's hard to separate which rules apply under which situations. Everything is all muddled together. The approach I've outlined separates the rules to make it clear when they should be applied.

As for maintaining code across projects, I try to encapsulate things as best I can. I move what I can into plugins that are easier to drop into new projects. As well, having things in the app_* files like AppController. That leaves (hopefully) everything else to be unique for that project (like that ever happens). I have svn repos for each project as well as the latest cake version, a dev version that I use to play with different techniques sandboxed away from other projects. As well, I set up repos for things like my plugins. That's worked out well so far. It'd be cooler to have an IDE or other GUI approach to managing this but I don't know of any.

Bruno Bergher said on July 23, 2008

I understand the need to separate the rules more clearly, but seems it's a trade-off determined by how many 'exceptions' you have?: if the model tends to use the default rules, the 'on' approach would be productive', but if each action uses a completely different rules, I certainly agree with your approach.

Your way of dealing with this shared code seems good as well. But you do have to copy and paste much of the basic structure of projects from time to time, right? That's about the same way I'm doing.

I've been considering creating a repository in which I'd keep all the plugins, for example, which could be referenced in each project's app/plugins folder, but it seems something that would break in no time. I also work if ActionScript, and the concept of a central library that is so simple for compiled languages is always something to be missed when working with PHP.

Thanks for sharing anyway : )

Kyle Neath said on July 23, 2008

My God, thanks for reminding me why I don't code in PHP any longer. You totally made my day :)

Jonathan Snook said on July 23, 2008

@Kyle Neath: Haha! I'm glad I could be of service. You bastard. ;)

Ben said on July 23, 2008

Thanks for this, I was wondering how validation would work across different actions. I've been working blindly, just hoping I would work out what to do when I write the edit action for users to edit their own details - where their username and certain other fields would not be editable by them. And unless I've completely misunderstood what you've presented here, this will be a great help.

This also got me thinking about something else, slightly off-topic, but perhaps you could point me in the right direction with this: Say you have a register action for users to sign up and then an edit action for them to change some of their signup details at a later stage. The edit action should naturally not allow them to change their username and so the associated view template would not have the corresponding form field. What is to stop a user from writing a custom form and including a username field in it? Surely when the action calls save() then the username field would be updated...? Do you know if cake has any "built-in" protection against this or would one have to manually unset these forbidden values from the data before calling save. Maybe I've missed the plot completely? This whole MVC thing is rather new to me.

Well, thanks for this great post anyway. Very helpful.

Derick said on July 23, 2008

I read your post and went ahead to share my approach as well. :)

@Ben: You could do as shown below so you simply specify fields to save.

$this->save($data, true, array('fields', 'to', 'save'));

Ben said on July 23, 2008

@Derick: Thanks... it's always the easy and logical step that I overlook...

Mark said on July 27, 2008

i left my validation inside the models, one in user.php and one in user_info.php
the "User" model validates, but before i want to save the data, i want to check if the "UserInfo" is valid as well. But it just skippes this second validation.
All data is inserted in the DB - although you could add all kinds of stuff to the "homepage" field of "UserData" right now.

USER MODEL:
class User extends AppModel {

var $name = 'User';
var $validate = array(
'username' => array(
'alphanumeric' => array(
'rule' => array('alphanumeric'),
'required' => true,
'message' => 'Username ungueltig'
),
'unique' => array(
'rule' => array('isUnique'),
'message' => 'Username gibt es schon'
)
),
'email' => array(
'email' => array(
'rule' => array('email'),
'required' => true,
'message' => 'Email ungueltig'
),
'unique' => array(
'rule' => array('isUnique'),
'message' => 'Email gibt es schon'
),
'between' => array(
'rule' => array('between', 4, 65),
'message' => 'Between 4 to 65 characters'
)
),
'password' => array(
'password_between' => array(
'rule' => array('between', 4, 65),
'required' => true,
'message' => 'Between 4 to 65 characters'
)
),
'pwd_repeat' => array(
'between' => array(
'rule' => array('between', 4, 65),
'required' => true,
'message' => 'Between 4 to 65 characters'
),
'identicalFieldValues' => array(
'rule' => array('identicalFieldValues','password'),
'message' => '** error **'
)
)
);
...

USERINFO MODEL:
class UserInfo extends AppModel {

var $name = 'UserInfo';
var $validate = array(
'homepage' => array('alphanumeric'),
'sex' => array('numeric'),
'zipcode' => array('alphanumeric'),
'city' => array('alphanumeric'),
'country_id' => array('numeric'),
'countries_province_id' => array('numeric'),
'job' => array('alphanumeric')
);
...

USERS CONTROLLER:
function register() {
$this->DarkAuth->redirectLoggedIn();

if (!empty($this->data)) {
$this->User->create();

$this->User->set($this->data['User']);
if ($this->User->validates() && $this->User->UserInfo->validates()) {
$this->data['User']['password']=DarkAuthComponent::hasher($this->data['User']['password']);

if ($this->User->save($this->data,false)) { // validation is already done
$this->data['UserInfo']['user_id'] = $this->User->id;
$this->User->UserInfo->save($this->data,false); // validation is already done (STILL NOT WORKING!)

$this->Session->setFlash(__('You have successfully registered', true));
$this->redirect(array('action'=>'registered'));
} else {
$this->Session->setFlash(__('Registration could not be continued', true));
}
} else {
$this->Session->setFlash(__('The Data could not be saved. Please, try again.', true));
}
}
//$groups = $this->User->Group->find('list');
//$this->set(compact('groups'));
}

juanfgs said on July 28, 2008

Wow!, I was writing something like this, you saved me a lot of work. (However I hate you because it seemed like a cool thing to do :P ).

Just kidding, you did an awesome work! thank you!

Richard@Home said on August 03, 2008

Finally got an opportunity to play with this.

Firstly, it's a very elegant solution but I might have hit a (minor) snag.

The validation information is not passed to the view. Which means the $form->input() output doesn't autmatically highlight required fields.

For example: If you create a model with the validation sets validateRegister and validateEdit and don't create a default validate then the Registration form doesn't highlight the fields in validateRegister marked as required=true.

Here's my solution: In AppModel:

function setValidate($rule = null) {

if ($rule == null) {

$rule = 'validate' . Inflector::camelize(Router::getParam('action'));

if (!isset($this->$rule)) {

$rule = "validate";

}

}

$this->validate = $this->$rule;

}

In your controller method:

function register() {

$this->User->setValidate();

if(!empty($this->data)) {

$this->User->set($this->data);

if ($this->User->validates()) {

}

}

}

$this->User->setValidate() uses the validation rule named after the current action ($validateRegister in this case)

$this->User->setValidate("foo") uses the validation rule $foo set in the model

If you leave out $this->User->setValidate, it defaults to using the default $validate

Richard@Home said on August 03, 2008

Minor enhancement to AppModel:

function AppModel() {

parent::__construct();

$this->setValidate();

}

Now you don't have to explicitly user $this->Model->setValidate() in your controller method

:-)

Richard@Home said on August 03, 2008

explicitly set*

Steve said on August 11, 2008

Just wanted to note that the following line:
$param = 'validate' . $validationSet;

Should be:
$param = 'validate' . $this->validationSet;

Frank said on August 29, 2008

Cheers, just the approach I was looking for. Just a note to other readers, as this caught me up, retrieve the invalid fields from the model using:

$this->User->validationErrors;

It might be obvious, but calling:

$this->User->invalidFields();

Will run the validation over again, not using the rules you specifed in validates(). This tripped me up, but otherwise works really well.

John said on September 12, 2008

Thanks for the article. I have been growing increasingly frustrated with Cake's current validation setup making it seemingly impossible for me to cater for different situations like registering, editing profile and changing password.

@Richard@Home: Is it possible to cater for more than one form on one page, each with different validation sets?

Josh Prowse said on November 16, 2008

This is fantastic! Do you think it would be hard to get it working like CodeIgniter, where all the validation arrays can be stored in a global $config variable according to the model/action string? (See "Associating a Controller Function with a Rule Group" just a bit down on this page: http://codeigniter.com/user_guide/libraries/form_validation.html#savingtoconfig)

Again, thanks!
:-j

Sorry, comments are closed for this post. If you have any further questions or comments, feel free to send them to me directly.