Welcome Guest, Not a member yet? Register   Sign In
Improved validation callbacks
#1

[eluser]TheFuzzy0ne[/eluser]
I was getting a bit cheesed off with having to use two underscores for private callbacks in a controller (must be my old age I guess), and also, I wanted a way to instantly be able to see which controller methods are callbacks, so I came up with this:

./system/application/libraries/MY_Form_validation.php
Code:
<?php

class MY_Form_validation extends CI_Form_validation {
    
    function _execute($row, $rules, $postdata = NULL, $cycles = 0)
    {
        // If the $_POST data is an array we will run a recursive call
        if (is_array($postdata))
        {
            foreach ($postdata as $key => $val)
            {
                $this->_execute($row, $rules, $val, $cycles);
                $cycles++;
            }
            
            return;
        }
        
        // --------------------------------------------------------------------

        // If the field is blank, but NOT required, no further tests are necessary
        $callback = FALSE;
        if ( ! in_array('required', $rules) AND is_null($postdata))
        {
            // Before we bail out, does the rule contain a callback?
            if (preg_match("/(callback_\w+)/", implode(' ', $rules), $match))
            {
                $callback = TRUE;
                $rules = (array('1' => $match[1]));
            }
            else
            {
                return;
            }
        }

        // --------------------------------------------------------------------
        
        // Isset Test. Typically this rule will only apply to checkboxes.
        if (is_null($postdata) AND $callback == FALSE)
        {
            if (in_array('isset', $rules, TRUE) OR in_array('required', $rules))
            {
                // Set the message type
                $type = (in_array('required', $rules)) ? 'required' : 'isset';
            
                if ( ! isset($this->_error_messages[$type]))
                {
                    if (FALSE === ($line = $this->CI->lang->line($type)))
                    {
                        $line = 'The field was not set';
                    }                            
                }
                else
                {
                    $line = $this->_error_messages[$type];
                }
                
                // Build the error message
                $message = sprintf($line, $this->_translate_fieldname($row['label']));

                // Save the error message
                $this->_field_data[$row['field']]['error'] = $message;
                
                if ( ! isset($this->_error_array[$row['field']]))
                {
                    $this->_error_array[$row['field']] = $message;
                }
            }
                    
            return;
        }

        // --------------------------------------------------------------------

        // Cycle through each rule and run it
        foreach ($rules As $rule)
        {
            $_in_array = FALSE;
            
            // We set the $postdata variable with the current data in our master array so that
            // each cycle of the loop is dealing with the processed data from the last cycle
            if ($row['is_array'] == TRUE AND is_array($this->_field_data[$row['field']]['postdata']))
            {
                // We shouldn't need this safety, but just in case there isn't an array index
                // associated with this cycle we'll bail out
                if ( ! isset($this->_field_data[$row['field']]['postdata'][$cycles]))
                {
                    continue;
                }
            
                $postdata = $this->_field_data[$row['field']]['postdata'][$cycles];
                $_in_array = TRUE;
            }
            else
            {
                $postdata = $this->_field_data[$row['field']]['postdata'];
            }

            // --------------------------------------------------------------------
    
            // Is the rule a callback?            
            $callback = FALSE;
            if (substr($rule, 0, 9) == 'callback_')
            {
                $rule = substr($rule, 9);
                $callback = TRUE;
            }
            
            // Strip the parameter (if exists) from the rule
            // Rules can contain a parameter: max_length[5]
            $param = FALSE;
            if (preg_match("/(.*?)\[(.*?)\]/", $rule, $match))
            {
                $rule    = $match[1];
                $param    = $match[2];
            }
            
            // Call the function that corresponds to the rule
            if ($callback === TRUE)
            {
# START EDIT
                $method = '';
                if (method_exists($this->CI, 'callback_' . $rule))
                {        
                    $method = $rule = 'callback_' . $rule;
                }
                else if (method_exists($this->CI, '_callback_' . $rule))
                {        
                    $method = $rule = '_callback_' . $rule;
                }
                else if (method_exists($this->CI, $rule))
                {
                    $method = $rule;
                }
                else
                {
                    continue;
                }
                
                // Run the function and grab the result
                $result = $this->CI->$method($postdata, $param);
# END EDIT
                // Re-assign the result to the master data array
                if ($_in_array == TRUE)
                {
                    $this->_field_data[$row['field']]['postdata'][$cycles] = (is_bool($result)) ? $postdata : $result;
                }
                else
                {
                    $this->_field_data[$row['field']]['postdata'] = (is_bool($result)) ? $postdata : $result;
                }

Contd...
#2

[eluser]TheFuzzy0ne[/eluser]
Code:
// If the field isn't required and we just processed a callback we'll move on...
                if ( ! in_array('required', $rules, TRUE) AND $result !== FALSE)
                {
                    return;
                }
            }
            else
            {                
                if ( ! method_exists($this, $rule))
                {
                    // If our own wrapper function doesn't exist we see if a native PHP function does.
                    // Users can use any native PHP function call that has one param.
                    if (function_exists($rule))
                    {
                        $result = $rule($postdata);
                                            
                        if ($_in_array == TRUE)
                        {
                            $this->_field_data[$row['field']]['postdata'][$cycles] = (is_bool($result)) ? $postdata : $result;
                        }
                        else
                        {
                            $this->_field_data[$row['field']]['postdata'] = (is_bool($result)) ? $postdata : $result;
                        }
                    }
                                        
                    continue;
                }

                $result = $this->$rule($postdata, $param);

                if ($_in_array == TRUE)
                {
                    $this->_field_data[$row['field']]['postdata'][$cycles] = (is_bool($result)) ? $postdata : $result;
                }
                else
                {
                    $this->_field_data[$row['field']]['postdata'] = (is_bool($result)) ? $postdata : $result;
                }
            }
                            
            // Did the rule test negatively?  If so, grab the error.
            if ($result === FALSE)
            {            
                if ( ! isset($this->_error_messages[$rule]))
                {
                    if (FALSE === ($line = $this->CI->lang->line($rule)))
                    {
                        $line = 'Unable to access an error message corresponding to your field name.';
                    }                        
                }
                else
                {
                    $line = $this->_error_messages[$rule];
                }

                // Build the error message
                $message = sprintf($line, $this->_translate_fieldname($row['label']), $param);

                // Save the error message
                $this->_field_data[$row['field']]['error'] = $message;
                
                if ( ! isset($this->_error_array[$row['field']]))
                {
                    $this->_error_array[$row['field']] = $message;
                }
                
                return;
            }
        }
    }
}
Now I can define a callback rule using callback_some_rule any my method will look like this:
Code:
function callback_some_rule($str)
{
    $this->form_validation->set_message('callback_some_rule', 'Uh-oh!');
    // ...
}

You can also define your callback like this if you want to make it prviate (you would still use the same rule name though, no leading underscore necessary):
Code:
function _callback_some_rule($str)
{
    $this->form_validation->set_message('_callback_some_rule', 'Uh-oh!');
    // ...
}

However I chose to customise the controller constructor, and add a method that denies access to any controller methods starting with "callback_".

./system/application/libraries/MY_Controller.php
Code:
<?php

class MY_Controller extends Controller {
    
    function MY_Controller()
    {
        parent::Controller();
        $this->_validate_controller_method();
    }
    
    function _validate_controller_method()
    {
        if (strncmp($this->uri->rsegment(2), 'callback_', 9) == 0)
        {
            show_404();
        }
    }
}

Of course, this will not break any of your old validation methods. It's fully backwards compatible.

Now all my callbacks are safe from external access, and I can see which methods are callbacks for the validation class.

I was contemplating taking it a step further, and prefixing validation methods with vcb instead, just in case anyone wanted a method that started with "callback_" to be accessible externally, but perhaps it's overkill?

If anyone likes this idea, and has any suggestions, please fire away.




Theme © iAndrew 2016 - Forum software by © MyBB