Changeset 2046

Show
Ignore:
Timestamp:
02/14/08 18:26:13 (5 months ago)
Author:
Shadowhand
Message:

Replacing Validation with the shiny new extensible version.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/system/libraries/Validation.php

    r1992 r2046  
    1010 * @license    http://kohanaphp.com/license.html 
    1111 */ 
    12 class Validation_Core { 
    13  
    14         // Instance count 
    15         private static $instances = 0; 
    16  
    17         // Currently validating field 
    18         public $current_field = ''; 
    19  
    20         // Enable or disable safe form errors 
    21         public $form_safe = FALSE; 
    22  
    23         // Error message format 
    24         public $error_format = '<p class="error">{message}</p>'; 
    25         public $newline_char = "\n"; 
    26  
    27         // Error messages 
    28         public $messages = array(); 
    29  
    30         // Field names, rules, and errors 
    31         protected $fields = array(); 
    32         protected $rules  = array(); 
     12class Validation_Core extends ArrayObject { 
     13 
     14        // Unique "any field" key 
     15        protected $any_field; 
     16 
     17        // Message output format 
     18        protected $message_format = '<p class="error">{message}</p>'; 
     19 
     20        // Filters 
     21        protected $pre_filters = array(); 
     22        protected $post_filters = array(); 
     23 
     24        // Rules and callbacks 
     25        protected $rules = array(); 
     26        protected $callbacks = array(); 
     27 
     28        // Errors 
    3329        protected $errors = array(); 
    34  
    35         // Data to validate 
    36         protected $data = array(); 
    37  
    38         // Result from validation rules 
    39         protected $result; 
    40  
    41         /** 
    42          * @param array  array to validate 
    43          */ 
    44         public function __construct( & $data = array()) 
    45         { 
    46                 $this->set_data($data); 
    47  
    48                 // Load the default error messages 
    49                 $this->messages = Kohana::lang('validation'); 
    50  
    51                 // Add one more instance to the count 
    52                 self::$instances++; 
    53  
    54                 Log::add('debug', 'Validation Library Initialized, instance '.self::$instances); 
    55         } 
    56  
    57         /** 
    58          * Magically gets a validation variable. This can be an error string or a 
    59          * data field, or an array of all field data. 
    60          * 
    61          * @param   string        Variable name 
    62          * @return  string|array  The variable contents or NULL if the variable does not exist 
    63          */ 
    64         public function __get($key) 
    65         { 
    66                 if ( ! isset($this->$key)) 
    67                 { 
    68                         if ($key === 'error_string') 
    69                         { 
    70                                 // Complete error message string 
    71                                 $messages = FALSE; 
    72                                 foreach(array_keys($this->errors) as $field) 
    73                                 { 
    74                                         $messages .= $this->__get($field.'_error'); 
    75                                 } 
    76                                 return $messages; 
    77                         } 
    78                         elseif (substr($key, -6) === '_error') 
    79                         { 
    80                                 // Get the field name 
    81                                 $field = substr($key, 0, -6); 
    82  
    83                                 // Return the error messages for this field 
    84                                 $messages = FALSE; 
    85                                 if (isset($this->errors[$field]) AND ! empty($this->errors[$field])) 
    86                                 { 
    87                                         foreach($this->errors[$field] as $error) 
     30        protected $messages = array(); 
     31 
     32        /** 
     33         * Creates a new Validation instance. 
     34         * 
     35         * @param   array   array to use for validation 
     36         * @return  object 
     37         */ 
     38        public static function factory($array = NULL) 
     39        { 
     40                return new Validation( ! is_array($array) ? $_POST : $array); 
     41        } 
     42 
     43        /** 
     44         * Sets the unique "any field" key and creates an ArrayObject from the 
     45         * passed array. 
     46         * 
     47         * @param   array   array to validate 
     48         * @return  void 
     49         */ 
     50        public function __construct(array $array) 
     51        { 
     52                // Set a dynamic, unique "any field" key 
     53                $this->any_field = uniqid(NULL, TRUE); 
     54 
     55                parent::__construct($array, ArrayObject::ARRAY_AS_PROPS | ArrayObject::STD_PROP_LIST); 
     56        } 
     57 
     58        /** 
     59         * Returns the ArrayObject array values. 
     60         * 
     61         * @return  array 
     62         */ 
     63        public function as_array() 
     64        { 
     65                return $this->getArrayCopy(); 
     66        } 
     67 
     68        /** 
     69         * Set the format of message strings. 
     70         * 
     71         * @chainable 
     72         * @param   string   new message format 
     73         * @return  object 
     74         */ 
     75        public function message_format($str) 
     76        { 
     77                if (strpos($str, '{message}') === FALSE) 
     78                        throw new Kohana_Exception('validation.error_format'); 
     79 
     80                // Set the new message format 
     81                $this->message_format = $str; 
     82 
     83                return $this; 
     84        } 
     85 
     86        /** 
     87         * Sets or returns the message for an input. 
     88         * 
     89         * @chainable 
     90         * @param   string   input key 
     91         * @param   string   message to set 
     92         * @return  string|object 
     93         */ 
     94        public function message($input, $message = NULL) 
     95        { 
     96                if ($message === NULL) 
     97                { 
     98                        // Return nothing if no message exists 
     99                        if (empty($this->messages[$input])) 
     100                                return ''; 
     101 
     102                        // Return the HTML message string 
     103                        return str_replace('{message}', $this->messages[$input], $this->message_format); 
     104                } 
     105                else 
     106                { 
     107                        $this->messages[$input] = $message; 
     108                } 
     109 
     110                return $this; 
     111        } 
     112 
     113        /** 
     114         * Add a pre-filter to one or more inputs. 
     115         * 
     116         * @chainable 
     117         * @param   callback  filter 
     118         * @param   string    fields to apply filter to, use TRUE for all fields 
     119         * @return  object 
     120         */ 
     121        public function pre_filter($filter, $field = TRUE) 
     122        { 
     123                if ( ! is_callable($filter)) 
     124                        throw new Kohana_Exception('validation.filter_not_callable'); 
     125 
     126                if ($field === TRUE) 
     127                { 
     128                        // Handle "any field" filters 
     129                        $fields = $this->any_field; 
     130                } 
     131                else 
     132                { 
     133                        // Add the filter to specific inputs 
     134                        $fields = func_get_args(); 
     135                        $fields = array_slice($fields, 1); 
     136                } 
     137 
     138                foreach ($fields as $field) 
     139                { 
     140                        // Add the filter to specified field 
     141                        $this->pre_filters[$field][] = $filter; 
     142                } 
     143 
     144                return $this; 
     145        } 
     146 
     147        /** 
     148         * Add a post-filter to one or more inputs. 
     149         * 
     150         * @chainable 
     151         * @param   callback  filter 
     152         * @param   string    fields to apply filter to, use TRUE for all fields 
     153         * @return  object 
     154         */ 
     155        public function post_filter($filter, $field = TRUE) 
     156        { 
     157                if ( ! is_callable($filter, TRUE)) 
     158                        throw new Kohana_Exception('validation.filter_not_callable'); 
     159 
     160                if ($field === TRUE) 
     161                { 
     162                        // Handle "any field" filters 
     163                        $fields = $this->any_field; 
     164                } 
     165                else 
     166                { 
     167                        // Add the filter to specific inputs 
     168                        $fields = func_get_args(); 
     169                        $fields = array_slice($fields, 1); 
     170                } 
     171 
     172                foreach ($fields as $field) 
     173                { 
     174                        // Add the filter to specified field 
     175                        $this->post_filters[$field][] = $filter; 
     176                } 
     177 
     178                return $this; 
     179        } 
     180 
     181        /** 
     182         * Add rules to a field. Rules are callbacks or validation methods. Rules can 
     183         * only return TRUE or FALSE. 
     184         * 
     185         * @chainable 
     186         * @param   string    field name 
     187         * @param   callback  rules (unlimited number) 
     188         * @return  object 
     189         */ 
     190        public function add_rules($field, $rules) 
     191        { 
     192                // Handle "any field" filters 
     193                ($field === TRUE) and $field = $this->any_field; 
     194 
     195                // Get the rules 
     196                $rules = func_get_args(); 
     197                $rules = array_slice($rules, 1); 
     198 
     199                foreach ($rules as $rule) 
     200                { 
     201                        // Rule arguments 
     202                        $args = NULL; 
     203 
     204                        if (is_string($rule)) 
     205                        { 
     206                                if (preg_match('/^([^\[]++)\[(.+)\]$/', $rule, $matches)) 
     207                                { 
     208                                        // Split the rule into the function and args 
     209                                        $rule = $matches[1]; 
     210                                        $args = preg_split('/(?<!\\\\),\s*/', $matches[2]); 
     211                                } 
     212 
     213                                if (method_exists($this, $rule)) 
     214                                { 
     215                                        // Make the rule a valid callback 
     216                                        $rule = array($this, $rule); 
     217                                } 
     218                        } 
     219 
     220                        if ( ! is_callable($rule, TRUE)) 
     221                                throw new Kohana_Exception('validation.rule_not_callable'); 
     222 
     223                        // Add the rule to specified field 
     224                        $this->rules[$field][] = array($rule, $args); 
     225                } 
     226 
     227                return $this; 
     228        } 
     229 
     230        /** 
     231         * Add callbacks to a field. Callbacks must accept the Validation object 
     232         * and the input name. Callback returns are not processed. 
     233         * 
     234         * @chainable 
     235         * @param   string     field name 
     236         * @param   callbacks  callbacks (unlimited number) 
     237         * @return  object 
     238         */ 
     239        public function add_callbacks($field, $callbacks) 
     240        { 
     241                // Handle "any field" filters 
     242                ($field === TRUE) and $field = $this->any_field; 
     243 
     244                if (func_get_args() > 2) 
     245                { 
     246                        // Multiple callback 
     247                        $callbacks = func_get_args(); 
     248                        $callbacks = array_slice($callbacks, 1); 
     249                } 
     250                else 
     251                { 
     252                        // Only one callback 
     253                        $callbacks = array($callbacks); 
     254                } 
     255 
     256                foreach ($callbacks as $callback) 
     257                { 
     258                        if ( ! is_callable($callback, TRUE)) 
     259                                throw new Kohana_Exception('validation.callback_not_callable'); 
     260 
     261                        // Add the filter to specified field 
     262                        $this->callbacks[$field][] = $callback; 
     263                } 
     264 
     265                return $this; 
     266        } 
     267 
     268        /** 
     269         * Validate by processing pre-filters, rules, callbacks, and post-filters. 
     270         * All fields that have filters, rules, or callbacks will be initialized if 
     271         * they are undefined. Validation will only be run if there is data already 
     272         * in the array. 
     273         * 
     274         * @return bool 
     275         */ 
     276        public function validate() 
     277        { 
     278                // All the fields that are being validated 
     279                $all_fields = array_unique(array_merge 
     280                ( 
     281                        array_keys($this->pre_filters), 
     282                        array_keys($this->rules), 
     283                        array_keys($this->callbacks), 
     284                        array_keys($this->post_filters) 
     285                )); 
     286 
     287                // Only run validation when POST data exists 
     288                $run_validation = (count($this) > 0); 
     289 
     290                foreach ($all_fields as $i => $field) 
     291                { 
     292                        if ($field === $this->any_field) 
     293                        { 
     294                                // Remove "any field" from the list of fields 
     295                                unset($all_fields[$i]); 
     296                                continue; 
     297                        } 
     298 
     299                        // Make sure all fields are defined 
     300                        isset($this[$field]) or $this[$field] = NULL; 
     301                } 
     302 
     303                if ($run_validation === FALSE) 
     304                        return FALSE; 
     305 
     306                // Reset all fields to ALL defined fields 
     307                $all_fields = array_keys($this->getArrayCopy()); 
     308 
     309                foreach ($this->pre_filters as $field => $calls) 
     310                { 
     311                        foreach ($calls as $func) 
     312                        { 
     313                                if ($field === $this->any_field) 
     314                                { 
     315                                        foreach ($all_fields as $f) 
    88316                                        { 
    89                                                 // Replace the message with the error in the html error string 
    90                                                 $messages .= str_replace('{message}', $error, $this->error_format).$this->newline_char
     317                                                // Process each filter 
     318                                                $this[$f] = is_array($this[$f]) ? array_map($func, $this[$f]) : call_user_func($func, $this[$f])
    91319                                        } 
    92320                                } 
    93                                 return $messages; 
    94                         } 
    95                         elseif (isset($this->data[$key])) 
    96                         { 
    97                                 return $this->data[$key]; 
    98                         } 
    99                         elseif ($key === 'data_array') 
    100                         { 
    101                                 $data = array(); 
    102                                 foreach (array_keys($this->rules) as $key) 
    103                                 { 
    104                                         if (isset($this->data[$key])) 
     321                                else 
     322                                { 
     323                                        // Process each filter 
     324                                        $this[$field] = is_array($this[$field]) ? array_map($func, $this[$field]) : call_user_func($func, $this[$field]); 
     325                                } 
     326                        } 
     327                } 
     328 
     329                foreach ($this->rules as $field => $calls) 
     330                { 
     331                        foreach ($calls as $call) 
     332                        { 
     333                                // Split the rule into function and args 
     334                                list($func, $args) = $call; 
     335 
     336                                if ($field === $this->any_field) 
     337                                { 
     338                                        foreach ($all_fields as $f) 
    105339                                        { 
    106                                                 $data[$key] = $this->data[$key]; 
    107                                         } 
    108                                 } 
    109                                 return $data; 
    110                         } 
    111                 } 
    112         } 
    113  
    114         /** 
    115          * This function takes an array of key names, rules, and field names as 
    116          * input and sets internal field information. 
    117          * 
    118          * @param   string|array Key names 
    119          * @param   string       Rules 
    120          * @param   string       Field name 
    121          * @return  void 
    122          */ 
    123         public function set_rules($data, $rules = '', $field = FALSE) 
    124         { 
    125                 // Normalize rules to an array 
    126                 if ( ! is_array($data)) 
    127                 { 
    128                         if ($rules == '') return FALSE; 
    129  
    130                         // Make data into an array 
    131                         $data = array($data => array($field, $rules)); 
    132                 } 
    133  
    134                 // Set the field information 
    135                 foreach ($data as $name => $rules) 
    136                 { 
    137                         if (is_array($rules)) 
    138                         { 
    139                                 if (count($rules) > 1) 
    140                                 { 
    141                                         $field = current($rules); 
    142                                         $rules = next($rules); 
    143                                 } 
    144                                 else 
    145                                 { 
    146                                         $rules = current($rules); 
    147                                 } 
    148                         } 
    149  
    150                         // Empty field names default to the name of the element 
    151                         $this->fields[$name] = empty($field) ? $name : $field; 
    152                         $this->rules[$name]  = $rules; 
    153  
    154                         // Prevent fields from getting the wrong name 
    155                         unset($field); 
    156                 } 
    157         } 
    158  
    159         /** 
    160          * Lets users set their own error messages on the fly. 
    161          * Note - The key name has to match the function name that it corresponds to. 
    162          * 
    163          * @param   string  Function name 
    164          * @param   string  Error message 
    165          * @return  void 
    166          */ 
    167         public function set_message($func, $message = '') 
    168         { 
    169                 if ( ! is_array($func)) 
    170                 { 
    171                         $func = array($func => $message); 
    172                 } 
    173  
    174                 foreach($func as $name => $message) 
    175                 { 
    176                         $this->messages[$name] = $message; 
    177                 } 
    178         } 
    179  
    180         /** 
    181          * @param   array  Data to validate 
    182          * @return  void 
    183          */ 
    184         public function set_data( & $data) 
    185         { 
    186                 if ( ! empty($data) AND is_array($data)) 
    187                 { 
    188                         $this->data =& $data; 
    189                 } 
    190                 else 
    191                 { 
    192                         $this->data =& $_POST; 
    193                 } 
    194         } 
    195  
    196         /** 
    197          * Allows the user to change the error message format. Error formats must 
    198          * contain the string "{message}" or Kohana_Exception will be triggered. 
    199          * 
    200          * @param   string  Error message format   
    201          * @return  void 
    202          */ 
    203         public function error_format($string = '') 
    204         { 
    205                 if (strpos((string) $string, '{message}') === FALSE) 
    206                         throw new Kohana_Exception('validation.error_format'); 
    207  
    208                 $this->error_format = $string; 
    209         } 
    210  
    211         /** 
    212          * @param   string  Function name 
    213          * @param   string  field name 
    214          * @return  void 
    215          */ 
    216         public function add_error($func, $field) 
    217         { 
    218                 // Set the friendly field name 
    219                 $friendly = isset($this->fields[$field]) ? $this->fields[$field] : $field; 
    220  
    221                 // Fetch the message 
    222                 $message = isset($this->messages[$func]) ? $this->messages[$func] : $this->messages['unknown_error']; 
    223  
    224                 // Replacements in strings 
    225                 $replace = array_slice(func_get_args(), 1); 
    226  
    227                 if ( ! empty($replace) AND $replace[0] === $field) 
    228                 { 
    229                         // Add the friendly name instead of the field name 
    230                         $replace[0] = $friendly; 
    231                 } 
    232  
    233                 // Add the field name into the message, if there is a place for it 
    234                 $message = (strpos($message, '%s') !== FALSE) ? vsprintf($message, $replace) : $message; 
    235  
    236                 $this->errors[$field][] = $message; 
    237         } 
    238  
    239         /** 
    240          * This function does all the work. 
    241          * 
    242          * @return  boolean  The validation result 
    243          */ 
    244         public function run() 
    245         { 
    246                 // Do we even have any data to process?  Mm? 
    247                 if (count($this->data) == 0 OR count($this->rules) == 0) 
    248                 { 
    249                         return FALSE; 
    250                 } 
    251  
    252                 // Cycle through the rules and test for errors 
    253                 foreach ($this->rules as $field => $rules) 
    254                 { 
    255                         // Set the current field, for other functions to use 
    256                         $this->current_field = $field; 
    257  
    258                         // Insert uploads into the data 
    259                         if (strpos($rules, 'upload') !== FALSE AND isset($_FILES[$field])) 
    260                         { 
    261                                 if (is_array($_FILES[$field]['error'])) 
    262                                 { 
    263                                         foreach($_FILES[$field]['error'] as $error) 
    264                                         { 
    265                                                 if ($error !== UPLOAD_ERR_NO_FILE) 
     340                                                // Prevent other rules from running when this field already has errors 
     341                                                if ( ! empty($this->errors[$f])) break; 
     342 
     343                                                // Don't process rules on empty fields 
     344                                                if (($func[1] !== 'required' AND $func[1] !== 'matches') AND empty($this[$f])) 
     345                                                        continue; 
     346 
     347                                                // Run each rule 
     348                                                if ( ! call_user_func($func, $this[$f], $args)) 
    266349                                                { 
    267                                                         $this->data[$field] = $_FILES[$field]; 
    268                                                         break; 
     350                                                        $this->errors[$f] = is_array($func) ? $func[1] : $func; 
    269351                                                } 
    270352                                        } 
    271353                                } 
    272                                 elseif ($_FILES[$field]['error'] !== UPLOAD_ERR_NO_FILE) 
    273                                 { 
    274                                         $this->data[$field] = $_FILES[$field]; 
    275                                 } 
    276                         } 
    277  
    278                         // Process empty fields 
    279                         if ( ! isset($this->data[$field]) OR $this->data[$field] == NULL) 
    280                         { 
    281                                 // This field is required 
    282                                 if (strpos($rules, 'required') !== FALSE) 
    283                                 { 
    284                                         $this->add_error('required', $field); 
    285                                 } 
    286                                 continue; 
    287                         } 
    288  
    289                         // Loop through the rules and process each one 
    290                         foreach(explode('|', $rules) as $rule) 
    291                         { 
    292                                 // To properly handle recursion 
    293                                 $this->run_rule($rule, $field); 
    294  
    295                                 // Stop validating when there is an error 
    296                                 if ($this->result === FALSE) 
    297                                         break; 
    298                         } 
    299                 } 
    300  
    301                 // Run validation finished Event and return 
    302                 if (count($this->errors) == 0) 
    303                 { 
    304                         Event::run('validation.success', $this->data); 
    305                         return TRUE; 
     354                                else 
     355                                { 
     356                                        // Prevent other rules from running when this field already has errors 
     357                                        if ( ! empty($this->errors[$field])) break; 
     358 
     359                                        // Don't process rules on empty fields 
     360                                        if (($func[1] !== 'required' AND $func[1] !== 'matches') AND empty($this[$field])) 
     361                                                continue; 
     362 
     363                                        // Run each rule 
     364                                        if ( ! call_user_func($func, $this[$field], $args)) 
     365                                        { 
     366                                                $this->errors[$field] = is_array($func) ? $func[1] : $func; 
     367                                                // Stop after an error is found 
     368                                                break; 
     369                                        } 
     370                                } 
     371                        } 
     372                } 
     373 
     374                foreach ($this->callbacks as $field => $calls) 
     375                { 
     376                        foreach ($calls as $func) 
     377                        { 
     378                                if ($field === $this->any_field) 
     379                                { 
     380                                        foreach ($all_fields as $f) 
     381                                        { 
     382                                                // Execute the callback 
     383                                                call_user_func($func, $this, $f); 
     384 
     385                                                // Stop after an error is found 
     386                                                if ( ! empty($errors[$f])) break 2; 
     387                                        } 
     388                                } 
     389                                else 
     390                                { 
     391                                        // Execute the callback 
     392                                        call_user_func($func, $this, $field); 
     393 
     394                                        // Stop after an error is found 
     395                                        if ( ! empty($errors[$f])) break; 
     396                                } 
     397                        } 
     398                } 
     399 
     400                foreach ($this->post_filters as $field => $calls) 
     401                { 
     402                        foreach ($calls as $func) 
     403                        { 
     404                                if ($field === $this->any_field) 
     405                                { 
     406                                        foreach ($all_fields as $f) 
     407                                        { 
     408                                                // Process each filter 
     409                                                $this[$f] = is_array($this[$f]) ? array_map($func, $this[$f]) : call_user_func($func, $this[$f]); 
     410                                        } 
     411                                } 
     412                                else 
     413                                { 
     414                                        // Process each filter 
     415                                        $this[$field] = is_array($this[$field]) ? array_map($func, $this[$field]) : call_user_func($func, $this[$field]); 
     416                                } 
     417                        } 
     418                } 
     419 
     420                // Return TRUE if there are no errors 
     421                return (count($this->errors) === 0); 
     422        } 
     423 
     424        /** 
     425         * Add an error to an input. 
     426         * 
     427         * @chainable 
     428         * @param   string  input name 
     429         * @param   string  unique error name 
     430         * @return  object 
     431         */ 
     432        public function add_error($field, $name) 
     433        { 
     434                if (isset($this[$field])) 
     435                { 
     436                        $this->errors[$field] = $name; 
     437                } 
     438 
     439                return $this; 
     440        } 
     441 
     442        /** 
     443         * Return the errors array. 
     444         * 
     445         * @return array 
     446         */ 
     447        public function errors() 
     448        { 
     449                return $this->errors; 
     450        } 
     451 
     452        /** 
     453         * Rule: required. Generates an error if the field has an empty value. 
     454         * 
     455         * @param   mixed   input value 
     456         * @return  bool 
     457         */ 
     458        public function required($str) 
     459        { 
     460                return ! ($str === '' OR $str === NULL OR $str === FALSE OR (is_array($str) AND empty($str))); 
     461        } 
     462 
     463        /** 
     464         * Rule: matches. Generates an error if the field does not match one or more 
     465         * other fields. 
     466         * 
     467         * @param   mixed   input value 
     468         * @param   array   input names to match against 
     469         * @return  bool 
     470         */ 
     471        public function matches($str, array $inputs) 
     472        { 
     473                foreach ($inputs as $key) 
     474                { 
     475                        if ($str !== (isset($this[$key]) ? $this[$key] : NULL)) 
     476                                return FALSE; 
     477                } 
     478 
     479                return TRUE; 
     480        } 
     481 
     482        /** 
     483         * Rule: length. Generates an error if the field is too long or too short. 
     484         * 
     485         * @param   mixed   input value 
     486         * @param   array   minimum, maximum, or exact length to match 
     487         * @return  bool 
     488         */ 
     489        public function length($str, array $length) 
     490        { 
     491                if ( ! is_string($str)) 
     492                        return FALSE; 
     493 
     494                $size = strlen($str); 
     495                $status = FALSE; 
     496 
     497                if (count($length) > 1) 
     498                { 
     499                        list ($min, $max) = $length; 
     500 
     501                        if ($size >= $min AND $size <= $max) 
     502                        { 
     503                                $status = TRUE; 
     504                        } 
    306505                } 
    307506                else 
    308507                { 
    309                         Event::run('validation.failure', $this->data); 
    310                         return FALSE; 
    311                 } 
    312         } 
    313  
    314         /** 
    315          * Handles recursively calling rules on arrays of data. 
    316          * 
    317          * @param   string  Validation rule to be run on the data 
    318          * @param   string  Name of field 
    319          * @return  void 
    320          */ 
    321         protected function run_rule($rule, $field) 
    322         { 
    323                 // Use key_string to extract the field data 
    324                 $data = Kohana::key_string($field, $this->data); 
    325  
    326                 // Make sure that data input is not upload data 
    327                 if (is_array($data) AND ! (isset($data['tmp_name']) AND isset($data['error']))) 
    328                 { 
    329                         foreach($data as $key => $value) 
    330                         { 
    331                                 // Recursion is fun! 
    332                                 $this->run_rule($rule, $field.'.'.$key); 
    333  
    334                                 if ($this->result === FALSE) 
    335                                         break; 
    336                         } 
    337                 } 
    338                 else 
    339                 { 
    340                         if (strpos($rule, '=') === 0) 
    341                         { 
    342                                 $rule               = substr($rule, 1); 
    343                                 $this->data[$field] = $rule($data); 
    344                                 return; 
    345                         } 
    346  
    347                         // Handle callback rules 
    348                         $callback = FALSE; 
    349                         if (preg_match('/callback_(.+)/', $rule, $match)) 
    350                         { 
    351                                 $callback = $match[1]; 
    352                         } 
    353  
    354                         // Handle params 
    355                         $params = FALSE; 
    356                         if (preg_match('/([^\[]*+)\[(.+)\]/', $rule, $match)) 
    357                         { 
    358                                 $rule   = $match[1]; 
    359                                 $params = preg_split('/(?<!\\\\),/', $match[2]); 
    360                                 $params = str_replace('\,', ',', $params); 
    361                         } 
    362  
    363                         // Process this field with the rule 
    364                         if ($callback !== FALSE) 
    365                         { 
    366                                 if ( ! method_exists(Kohana::instance(), $callback)) 
    367                                         throw new Kohana_Exception('validation.invalid_rule', $callback); 
    368  
    369                                 $this->result = Kohana::instance()->$callback($data, $params); 
    370                         } 
    371                         elseif ($rule === 'matches' OR $rule === 'depends_on') 
    372                         { 
    373                                 $this->result = $this->$rule($field, $params); 
    374                         } 
    375                         elseif (method_exists($this, $rule)) 
    376                         { 
    377                                 $this->result = $this->$rule($data, $params); 
    378                         } 
    379                         elseif (is_callable($rule)) 
    380                         { 
    381                                 if (strpos($rule, '::') !== FALSE) 
    382                                 { 
    383                                         $this->result = call_user_func(explode('::', $rule), $data); 
    384                                 } 
    385                                 else 
    386         &nb