errors = array(); $this->messages = array(); } /** * Create a copy of the current validation rules and change the array. * * @chainable * @param array new array to validate * @return Validation */ public function copy(array $array) { $copy = clone $this; $copy->exchangeArray($array); return $copy; } /** * Returns an array of all the field names that have filters, rules, or callbacks. * * @return array */ public function field_names() { // All the fields that are being validated $fields = array_keys(array_merge ( $this->pre_filters, $this->rules, $this->callbacks, $this->post_filters )); // Remove wildcard fields $fields = array_diff($fields, array('*')); return $fields; } /** * Returns the array values of the current object. * * @return array */ public function as_array() { return $this->getArrayCopy(); } /** * Returns the ArrayObject values, removing all inputs without rules. * To choose specific inputs, list the field name as arguments. * * @param boolean return only fields with filters, rules, and callbacks * @return array */ public function safe_array() { // Load choices $choices = func_get_args(); $choices = empty($choices) ? NULL : array_combine($choices, $choices); // Get field names $fields = $this->field_names(); $safe = array(); foreach ($fields as $field) { if ($choices === NULL OR isset($choices[$field])) { if (isset($this[$field])) { $value = $this[$field]; if (is_object($value)) { // Convert the value back into an array $value = $value->getArrayCopy(); } } else { // Even if the field is not in this array, it must be set $value = NULL; } // Add the field to the array $safe[$field] = $value; } } return $safe; } /** * Add additional rules that will forced, even for empty fields. All arguments * passed will be appended to the list. * * @chainable * @param string rule name * @return object */ public function allow_empty_rules($rules) { // Any number of args are supported $rules = func_get_args(); // Merge the allowed rules $this->empty_rules = array_merge($this->empty_rules, $rules); return $this; } /** * Sets or overwrites the label name for a field. * * @param string field name * @param string label * @return $this */ public function label($field, $label = NULL) { if ($label === NULL AND ($field !== TRUE OR $field !== '*') AND ! isset($this->labels[$field])) { // Set the field label to the field name $this->labels[$field] = ucfirst(preg_replace('/[^\pL]+/u', ' ', $field)); } elseif ($label !== NULL) { // Set the label for this field $this->labels[$field] = $label; } return $this; } /** * Sets labels using an array. * * @param array list of field => label names * @return $this */ public function labels(array $labels) { $this->labels = $labels + $this->labels; return $this; } /** * Converts a filter, rule, or callback into a fully-qualified callback array. * * @return mixed */ protected function callback($callback) { if (is_string($callback)) { if (strpos($callback, '::') !== FALSE) { $callback = explode('::', $callback); } elseif (function_exists($callback)) { // No need to check if the callback is a method $callback = $callback; } elseif (method_exists($this, $callback)) { // The callback exists in Validation $callback = array($this, $callback); } elseif (method_exists('valid', $callback)) { // The callback exists in valid:: $callback = array('valid', $callback); } } if ( ! is_callable($callback, FALSE)) { if (is_array($callback)) { if (is_object($callback[0])) { // Object instance syntax $name = get_class($callback[0]).'->'.$callback[1]; } else { // Static class syntax $name = $callback[0].'::'.$callback[1]; } } else { // Function syntax $name = $callback; } throw new Kohana_Exception('Callback %name% used for Validation is not callable', array('%name%' => $name)); } return $callback; } /** * Add a pre-filter to one or more inputs. Pre-filters are applied before * rules or callbacks are executed. * * @chainable * @param callback filter * @param string fields to apply filter to, use TRUE for all fields * @return object */ public function pre_filter($filter, $field = TRUE) { if ($field === TRUE OR $field === '*') { // Use wildcard $fields = array('*'); } else { // Add the filter to specific inputs $fields = func_get_args(); $fields = array_slice($fields, 1); } // Convert to a proper callback $filter = $this->callback($filter); foreach ($fields as $field) { // Add the filter to specified field $this->pre_filters[$field][] = $filter; } return $this; } /** * Add a post-filter to one or more inputs. Post-filters are applied after * rules and callbacks have been executed. * * @chainable * @param callback filter * @param string fields to apply filter to, use TRUE for all fields * @return object */ public function post_filter($filter, $field = TRUE) { if ($field === TRUE) { // Use wildcard $fields = array('*'); } else { // Add the filter to specific inputs $fields = func_get_args(); $fields = array_slice($fields, 1); } // Convert to a proper callback $filter = $this->callback($filter); foreach ($fields as $field) { // Add the filter to specified field $this->post_filters[$field][] = $filter; } return $this; } /** * Add rules to a field. Validation rules may only return TRUE or FALSE and * can not manipulate the value of a field. * * @chainable * @param string field name * @param callback rules (one or more arguments) * @return object */ public function add_rules($field, $rules) { // Get the rules $rules = func_get_args(); $rules = array_slice($rules, 1); // Set a default field label $this->label($field); if ($field === TRUE) { // Use wildcard $field = '*'; } foreach ($rules as $rule) { // Arguments for rule $args = NULL; // False rule $false_rule = FALSE; $rule_tmp = trim(is_string($rule) ? $rule : $rule[1]); // Should the rule return false? if ($rule_tmp !== ($rule_name = ltrim($rule_tmp, '! '))) { $false_rule = TRUE; } if (is_string($rule)) { // Use the updated rule name $rule = $rule_name; // Have arguments? if (preg_match('/^([^\[]++)\[(.+)\]$/', $rule, $matches)) { // Split the rule into the function and args $rule = $matches[1]; $args = preg_split('/(?array_fields[$field] = $field; } // Convert to a proper callback $rule = $this->callback($rule); // Add the rule, with args, to the field $this->rules[$field][] = array($rule, $args, $false_rule); } return $this; } /** * Add callbacks to a field. Callbacks must accept the Validation object * and the input name. Callback returns are not processed. * * @chainable * @param string field name * @param callbacks callbacks (unlimited number) * @return object */ public function add_callbacks($field, $callbacks) { // Get all callbacks as an array $callbacks = func_get_args(); $callbacks = array_slice($callbacks, 1); // Set a default field label $this->label($field); if ($field === TRUE) { // Use wildcard $field = '*'; } foreach ($callbacks as $callback) { // Convert to a proper callback $callback = $this->callback($callback); // Add the callback to specified field $this->callbacks[$field][] = $callback; } return $this; } /** * Validate by processing pre-filters, rules, callbacks, and post-filters. * All fields that have filters, rules, or callbacks will be initialized if * they are undefined. Validation will only be run if there is data already * in the array. * * @param object Validation object, used only for recursion * @param object name of field for errors * @return bool */ public function validate($object = NULL, $field_name = NULL) { if ($object === NULL) { // Use the current object $object = $this; } $array = $this->safe_array(); // Get all defined field names $fields = array_keys($array); foreach ($this->pre_filters as $field => $callbacks) { foreach ($callbacks as $callback) { if ($field === '*') { foreach ($fields as $f) { $array[$f] = is_array($array[$f]) ? array_map($callback, $array[$f]) : call_user_func($callback, $array[$f]); } } else { $array[$field] = is_array($array[$field]) ? array_map($callback, $array[$field]) : call_user_func($callback, $array[$field]); } } } foreach ($this->rules as $field => $callbacks) { foreach ($callbacks as $callback) { // Separate the callback, arguments and is false bool list ($callback, $args, $is_false) = $callback; // Function or method name of the rule $rule = is_array($callback) ? $callback[1] : $callback; if ($field === '*') { foreach ($fields as $f) { // Note that continue, instead of break, is used when // applying rules using a wildcard, so that all fields // will be validated. if (isset($this->errors[$f])) { // Prevent other rules from being evaluated if an error has occurred continue; } if (empty($array[$f]) AND ! in_array($rule, $this->empty_rules)) { // This rule does not need to be processed on empty fields continue; } $result = ($args === NULL) ? call_user_func($callback, $array[$f]) : call_user_func($callback, $array[$f], $args); if (($result == $is_false)) { $this->add_error($f, $rule, $args); // Stop validating this field when an error is found continue; } } } else { if (isset($this->errors[$field])) { // Prevent other rules from being evaluated if an error has occurred break; } if ( ! in_array($rule, $this->empty_rules) AND ! $this->required($array[$field])) { // This rule does not need to be processed on empty fields continue; } // Results of our test $result = ($args === NULL) ? call_user_func($callback, $array[$field]) : call_user_func($callback, $array[$field], $args); if (($result == $is_false)) { $rule = $is_false ? '!'.$rule : $rule; $this->add_error($field, $rule, $args); // Stop validating this field when an error is found break; } } } } foreach ($this->callbacks as $field => $callbacks) { foreach ($callbacks as $callback) { if ($field === '*') { foreach ($fields as $f) { // Note that continue, instead of break, is used when // applying rules using a wildcard, so that all fields // will be validated. if (isset($this->errors[$f])) { // Stop validating this field when an error is found continue; } call_user_func($callback, $this, $f); } } else { if (isset($this->errors[$field])) { // Stop validating this field when an error is found break; } call_user_func($callback, $this, $field); } } } foreach ($this->post_filters as $field => $callbacks) { foreach ($callbacks as $callback) { if ($field === '*') { foreach ($fields as $f) { $array[$f] = is_array($array[$f]) ? array_map($callback, $array[$f]) : call_user_func($callback, $array[$f]); } } else { $array[$field] = is_array($array[$field]) ? array_map($callback, $array[$field]) : call_user_func($callback, $array[$field]); } } } // Swap the array back into the object $this->exchangeArray($array); // Return TRUE if there are no errors return $this->errors === array(); } /** * Add an error to an input. * * @chainable * @param string input name * @param string unique error name * @param string arguments to pass to lang file * @return object */ public function add_error($field, $name, $args = NULL) { $this->errors[$field] = array($name, $args); return $this; } /** * Return the errors array. * * @param boolean load errors from a message file * @return array */ public function errors($file = NULL) { if ($file === NULL) { $errors = array(); foreach($this->errors as $field => $error) { $errors[$field] = $error[0]; } return $errors; } else { $errors = array(); foreach ($this->errors as $input => $error) { // Locations to check for error messages $error_locations = array ( "validation/{$file}.{$input}.{$error[0]}", "validation/{$file}.{$input}.default", "validation/default.{$error[0]}" ); if (($message = Kohana::message($error_locations[0])) !== $error_locations[0]) { // Found a message for this field and error } elseif (($message = Kohana::message($error_locations[1])) !== $error_locations[1]) { // Found a default message for this field } elseif (($message = Kohana::message($error_locations[2])) !== $error_locations[2]) { // Found a default message for this error } else { // No message exists, display the path expected $message = "validation/{$file}.{$input}.{$error[0]}"; } // Start the translation values list $values = array(':field' => __($this->labels[$input])); if ( ! empty($error[1])) { foreach ($error[1] as $key => $value) { // Add each parameter as a numbered value, starting from 1 $values[':param'.($key + 1)] = __($value); } } // Translate the message using the default language $errors[$input] = __($message, $values); } return $errors; } } /** * Rule: required. Generates an error if the field has an empty value. * * @param mixed input value * @return bool */ public function required($str) { if (is_object($str) AND $str instanceof ArrayObject) { // Get the array from the ArrayObject $str = $str->getArrayCopy(); } if (is_array($str)) { return ! empty($str); } else { return ! ($str === '' OR $str === NULL OR $str === FALSE); } } /** * Rule: matches. Generates an error if the field does not match one or more * other fields. * * @param mixed input value * @param array input names to match against * @return bool */ public function matches($str, array $inputs) { foreach ($inputs as $key) { if ($str !== (isset($this[$key]) ? $this[$key] : NULL)) return FALSE; } return TRUE; } /** * Rule: length. Generates an error if the field is too long or too short. * * @param mixed input value * @param array minimum, maximum, or exact length to match * @return bool */ public function length($str, array $length) { if ( ! is_string($str)) return FALSE; $size = mb_strlen($str); $status = FALSE; if (count($length) > 1) { list ($min, $max) = $length; if ($size >= $min AND $size <= $max) { $status = TRUE; } } else { $status = ($size === (int) $length[0]); } return $status; } /** * Rule: depends_on. Generates an error if the field does not depend on one * or more other fields. * * @param mixed field name * @param array field names to check dependency * @return bool */ public function depends_on($field, array $fields) { foreach ($fields as $depends_on) { if ( ! isset($this[$depends_on]) OR $this[$depends_on] == NULL) return FALSE; } return TRUE; } /** * Rule: chars. Generates an error if the field contains characters outside of the list. * * @param string field value * @param array allowed characters * @return bool */ public function chars($value, array $chars) { return ! preg_match('![^'.implode('', $chars).']!u', $value); } } // End Validation