Update 1.0.3 19/02/12: Updated checking for validation variables and methods before calling them.
Update 1.0.2 14/02/12: Removed static from libraries context. Might add static support later.
Update 1.0.1 13/02/12: Changed so CI Form_Validation rules are run first.

So, once again been tasked to play with the mighty CodeIgniter and having myself some fun. Except I still don't, for the love of god, understand why default validation is defined within controllers and not the models you're about to play with.

And so, I present some simple awesomeness to do two things.

Firstly we can define Form Validation rules within our Model class. Secondly, we can do all custom callbacks or complex rules within the model as well.

So under application/models/User.php

class User extends Model
{
    // Required to store Form_validation error Messages in.
    public $model_validate_error;
    
    // When declared $model_validate_<fieldname> will add additional rules
    public $model_validate_username = 'required|min_length[10]|max_length[50]';
    
    // Rest of your model logic here.  This will even work awesomely with PHPActiveRecord
    //
    //

    public function model_validate_username($string)
    {
        if (!validate_string_length_between($string, 4, 15)) {
            $this->model_validate_error = 'Username not between 4 and 15 characters';
            return FALSE;
        } else {
            return TRUE;
        }
    }
}

So here we're defining the rules for "username" and these are normal CodeIgniter Form_validation rules. To create custom functions (or callbacks within the model itself) you can use the public static function model_validate_. Do your logic within that function, set the validation error message if you have one, and return true/false. validate_string_length_between() was a little testing function I added to the bottom of my form validation extender, just cause.

Within your controller application/controllers/signup.php for exmaple

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Signup extends MY_Controller
{
    function index()
    {
        $this->load->library('form_validation');
        
        $this->load->model('User');  // Loading for model here for validation is not required, but I assume right after validation passed you'll be saving stuff.
        
        // Call validate_model functions in the [Modelname.field] style (sort of like is_unique for table checking)
        $this->form_validation->set_rules('username', 'Username', 'validate_model[User.username]');
        $this->form_validation->set_rules('password', 'Password', 'validate_model[User.password]');
        $this->form_validation->set_rules('email', 'Email Address', 'validate_model[User.email]');
        
        if ($this->form_validation->run()) {
            $this->view_data['message'] = "pass";
        } elseif ($this->input->post()) {
            $this->view_data['message'] = "failed";
        } else {
            $this->view_data['message'] = "not run";
        }
    }
}

One nice thing is that even after you declare validate_model on the email field, you don't need the rules already saved in the Model nor the extended callback function there. If it's there, it will be used. If it's not, it won't. Means you can setup your controllers today, and populate the rules tomorrow.

Your extending function application/libraries/MY_Form_validation.php

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');
 
class MY_Form_validation extends CI_Form_validation
{
     
    public function set_rules($field, $label = '', $rules = '')
    {
        // Check if "validate_model" is a rule within our set of rules.  If it is, load up the model's rules
        // within the rule-set and continue on to CI's class.
         
        if (strlen($rules) != 0) {
            $rule_list = explode('|', $rules);
            if ($rule_list != FALSE) {
                foreach ($rule_list as $rule) {
                    if (substr($rule, 0, 15) == 'validate_model[') {
                        // Found a validate_model ruling.  Grab the class name and field type.
                        $rule = substr($rule, 15);
                        $rule = substr($rule, 0, -1);
                        $functionName = explode ('.', $rule);
                        if (count($functionName == 2)) {
                            $modelName = $functionName[0];
                            $variableName = 'model_validate_' . $functionName[1];
                            
                            $this->CI->load->model($modelName, 'validate_model'); // Load the model
                            
                            if (isset($this->CI->validate_model->$variableName)) {
                                $rules = $this->CI->validate_model->$variableName . '|' . $rules;
                            }
                        }
                    }
                }
            }
        }
        // Always continue with the default CI set_rules even if we can't work anything additional out.
        parent::set_rules($field, $label, $rules);
    }
     
    public function validate_model($input = FALSE, $model_field = FALSE)
    {
        $functionName = explode('.', $model_field);
        if (count($functionName) != 2) {
            // Unable to work out 'model.function' to call.
            return;
        }
         
        $modelName = $functionName[0];
        $methodName = 'model_validate_' . $functionName[1];
         
        $this->CI->load->model($modelName, 'validate_model'); // Load the model if it's not already loaded.
         
        if (method_exists($this->CI->validate_model, $methodName)) {
            //$result = call_user_func(array($modelName, $methodName), $input);
            $result = $this->CI->validate_model->$methodName($input);
            if ($result) {
                return TRUE;
            } else {
                $this->set_message("validate_model", $this->CI->validate_model->model_validate_error);
                $this->CI->validate_model->model_validate_error = NULL;
                return FALSE;
            }
        } else {
            log_message('debug', "Unable to find validation rule: $modelName -> $methodName");
            return;
        }
    }
}

// Validation Functions
if (!function_exists('validate_string_length_between')) {
    function validate_string_length_between($string, $min, $max)
    {
        if (strlen($string) >= $min AND strlen($string) <= $max) {
            return TRUE;
        } else {
            return FALSE;
        }
    }
}

So! I hope with this simple stuff, makes your controllers much neater, and rules and validation far more reusable than either saving your rules in a config file, or copy/pasting your rules between controllers and extending Form_validation with custom function callbacks.

Feedback welcome, let me know!