/core/phpactiverecord/Validations.php
PHP | 833 lines | 454 code | 107 blank | 272 comment | 103 complexity | 01a6da497b3b4fd12201189bc5e3a363 MD5 | raw file
- <?php
- /**
- * These two classes have been <i>heavily borrowed</i> from Ruby on Rails' ActiveRecord so much that
- * this piece can be considered a straight port. The reason for this is that the vaildation process is
- * tricky due to order of operations/events. The former combined with PHP's odd typecasting means
- * that it was easier to formulate this piece base on the rails code.
- *
- * @package ActiveRecord
- */
- namespace ActiveRecord;
- use ActiveRecord\Model;
- use IteratorAggregate;
- use ArrayIterator;
- /**
- * Manages validations for a {@link Model}.
- *
- * This class isn't meant to be directly used. Instead you define
- * validators thru static variables in your {@link Model}. Example:
- *
- * <code>
- * class Person extends ActiveRecord\Model {
- * static $validates_length_of = array(
- * array('name', 'within' => array(30,100),
- * array('state', 'is' => 2)
- * );
- * }
- *
- * $person = new Person();
- * $person->name = 'Tito';
- * $person->state = 'this is not two characters';
- *
- * if (!$person->is_valid())
- * print_r($person->errors);
- * </code>
- *
- * @package ActiveRecord
- * @see Errors
- * @link http://www.phpactiverecord.org/guides/validations
- */
- class Validations
- {
- private $model;
- private $options = array();
- private $validators = array();
- private $record;
- private static $VALIDATION_FUNCTIONS = array(
- 'validates_presence_of',
- 'validates_size_of',
- 'validates_length_of',
- 'validates_inclusion_of',
- 'validates_exclusion_of',
- 'validates_format_of',
- 'validates_numericality_of',
- 'validates_uniqueness_of'
- );
- private static $DEFAULT_VALIDATION_OPTIONS = array(
- 'on' => 'save',
- 'allow_null' => false,
- 'allow_blank' => false,
- 'message' => null,
- );
- private static $ALL_RANGE_OPTIONS = array(
- 'is' => null,
- 'within' => null,
- 'in' => null,
- 'minimum' => null,
- 'maximum' => null,
- );
- private static $ALL_NUMERICALITY_CHECKS = array(
- 'greater_than' => null,
- 'greater_than_or_equal_to' => null,
- 'equal_to' => null,
- 'less_than' => null,
- 'less_than_or_equal_to' => null,
- 'odd' => null,
- 'even' => null
- );
- /**
- * Constructs a {@link Validations} object.
- *
- * @param Model $model The model to validate
- * @return Validations
- */
- public function __construct(Model $model)
- {
- $this->model = $model;
- $this->record = new Errors($this->model);
- $this->validators = array_intersect(array_keys(Reflections::instance()->get(get_class($this->model))->getStaticProperties()), self::$VALIDATION_FUNCTIONS);
- }
- /**
- * Returns validator data.
- *
- * @return array
- */
- public function rules()
- {
- $data = array();
- $reflection = Reflections::instance()->get(get_class($this->model));
- foreach ($this->validators as $validate)
- {
- $attrs = $reflection->getStaticPropertyValue($validate);
- foreach ($attrs as $attr)
- {
- $field = $attr[0];
- if (!isset($data[$field]) || !is_array($data[$field]))
- $data[$field] = array();
- $attr['validator'] = $validate;
- unset($attr[0]);
- array_push($data[$field],$attr);
- }
- }
- return $data;
- }
- /**
- * Runs the validators.
- *
- * @return Errors the validation errors if any
- */
- public function validate()
- {
- $reflection = Reflections::instance()->get(get_class($this->model));
- foreach ($this->validators as $validate)
- $this->$validate($reflection->getStaticPropertyValue($validate));
- $this->record->clear_model();
- return $this->record;
- }
- /**
- * Validates a field is not null and not blank.
- *
- * <code>
- * class Person extends ActiveRecord\Model {
- * static $validates_presence_of = array(
- * array('first_name'),
- * array('last_name')
- * );
- * }
- * </code>
- *
- * Available options:
- *
- * <ul>
- * <li><b>message:</b> custom error message</li>
- * </ul>
- *
- * @param array $attrs Validation definition
- */
- public function validates_presence_of($attrs)
- {
- $configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array('message' => Errors::$DEFAULT_ERROR_MESSAGES['blank'], 'on' => 'save'));
- foreach ($attrs as $attr)
- {
- $options = array_merge($configuration, $attr);
- $this->record->add_on_blank($options[0], $options['message']);
- }
- }
- /**
- * Validates that a value is included the specified array.
- *
- * <code>
- * class Car extends ActiveRecord\Model {
- * static $validates_inclusion_of = array(
- * array('fuel_type', 'in' => array('hyrdogen', 'petroleum', 'electric')),
- * );
- * }
- * </code>
- *
- * Available options:
- *
- * <ul>
- * <li><b>in/within:</b> attribute should/shouldn't be a value within an array</li>
- * <li><b>message:</b> custome error message</li>
- * </ul>
- *
- * @param array $attrs Validation definition
- */
- public function validates_inclusion_of($attrs)
- {
- $this->validates_inclusion_or_exclusion_of('inclusion', $attrs);
- }
- /**
- * This is the opposite of {@link validates_include_of}.
- *
- * @param array $attrs Validation definition
- * @see validates_inclusion_of
- */
- public function validates_exclusion_of($attrs)
- {
- $this->validates_inclusion_or_exclusion_of('exclusion', $attrs);
- }
- /**
- * Validates that a value is in or out of a specified list of values.
- *
- * @see validates_inclusion_of
- * @see validates_exclusion_of
- * @param string $type Either inclusion or exclusion
- * @param $attrs Validation definition
- */
- public function validates_inclusion_or_exclusion_of($type, $attrs)
- {
- $configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array('message' => Errors::$DEFAULT_ERROR_MESSAGES[$type], 'on' => 'save'));
- foreach ($attrs as $attr)
- {
- $options = array_merge($configuration, $attr);
- $attribute = $options[0];
- $var = $this->model->$attribute;
- if (isset($options['in']))
- $enum = $options['in'];
- elseif (isset($options['within']))
- $enum = $options['within'];
- if (!is_array($enum))
- array($enum);
- $message = str_replace('%s', $var, $options['message']);
- if ($this->is_null_with_option($var, $options) || $this->is_blank_with_option($var, $options))
- continue;
- if (('inclusion' == $type && !in_array($var, $enum)) || ('exclusion' == $type && in_array($var, $enum)))
- $this->record->add($attribute, $message);
- }
- }
- /**
- * Validates that a value is numeric.
- *
- * <code>
- * class Person extends ActiveRecord\Model {
- * static $validates_numericality_of = array(
- * array('salary', 'greater_than' => 19.99, 'less_than' => 99.99)
- * );
- * }
- * </code>
- *
- * Available options:
- *
- * <ul>
- * <li><b>only_integer:</b> value must be an integer (e.g. not a float)</li>
- * <li><b>even:</b> must be even</li>
- * <li><b>odd:</b> must be odd"</li>
- * <li><b>greater_than:</b> must be greater than specified number</li>
- * <li><b>greater_than_or_equal_to:</b> must be greater than or equal to specified number</li>
- * <li><b>equal_to:</b> ...</li>
- * <li><b>less_than:</b> ...</li>
- * <li><b>less_than_or_equal_to:</b> ...</li>
- * </ul>
- *
- * @param array $attrs Validation definition
- */
- public function validates_numericality_of($attrs)
- {
- $configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array('only_integer' => false));
- // Notice that for fixnum and float columns empty strings are converted to nil.
- // Validates whether the value of the specified attribute is numeric by trying to convert it to a float with Kernel.Float
- // (if only_integer is false) or applying it to the regular expression /\A[+\-]?\d+\Z/ (if only_integer is set to true).
- foreach ($attrs as $attr)
- {
- $options = array_merge($configuration, $attr);
- $attribute = $options[0];
- $var = $this->model->$attribute;
- $numericalityOptions = array_intersect_key(self::$ALL_NUMERICALITY_CHECKS, $options);
- if ($this->is_null_with_option($var, $options))
- continue;
- if (true === $options['only_integer'] && !is_integer($var))
- {
- if (!preg_match('/\A[+-]?\d+\Z/', (string)($var)))
- {
- if (isset($options['message']))
- $message = $options['message'];
- else
- $message = Errors::$DEFAULT_ERROR_MESSAGES['not_a_number'];
- $this->record->add($attribute, $message);
- continue;
- }
- }
- else
- {
- if (!is_numeric($var))
- {
- $this->record->add($attribute, Errors::$DEFAULT_ERROR_MESSAGES['not_a_number']);
- continue;
- }
- $var = (float)$var;
- }
- foreach ($numericalityOptions as $option => $check)
- {
- $option_value = $options[$option];
- if ('odd' != $option && 'even' != $option)
- {
- $option_value = (float)$options[$option];
- if (!is_numeric($option_value))
- throw new ValidationsArgumentError("$option must be a number");
- if (isset($options['message']))
- $message = $options['message'];
- else
- $message = Errors::$DEFAULT_ERROR_MESSAGES[$option];
- $message = str_replace('%d', $option_value, $message);
- if ('greater_than' == $option && !($var > $option_value))
- $this->record->add($attribute, $message);
- elseif ('greater_than_or_equal_to' == $option && !($var >= $option_value))
- $this->record->add($attribute, $message);
- elseif ('equal_to' == $option && !($var == $option_value))
- $this->record->add($attribute, $message);
- elseif ('less_than' == $option && !($var < $option_value))
- $this->record->add($attribute, $message);
- elseif ('less_than_or_equal_to' == $option && !($var <= $option_value))
- $this->record->add($attribute, $message);
- }
- else
- {
- if (isset($options['message']))
- $message = $options['message'];
- else
- $message = Errors::$DEFAULT_ERROR_MESSAGES[$option];
- if ( ('odd' == $option && !( Utils::is_odd($var))) || ('even' == $option && ( Utils::is_odd($var))))
- $this->record->add($attribute, $message);
- }
- }
- }
- }
- /**
- * Alias of {@link validates_length_of}
- *
- * @param array $attrs Validation definition
- */
- public function validates_size_of($attrs)
- {
- $this->validates_length_of($attrs);
- }
- /**
- * Validates that a value is matches a regex.
- *
- * <code>
- * class Person extends ActiveRecord\Model {
- * static $validates_format_of = array(
- * array('email', 'with' => '/^.*?@.*$/')
- * );
- * }
- * </code>
- *
- * Available options:
- *
- * <ul>
- * <li><b>with:</b> a regular expression</li>
- * <li><b>message:</b> custom error message</li>
- * </ul>
- *
- * @param array $attrs Validation definition
- */
- public function validates_format_of($attrs)
- {
- $configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array('message' => Errors::$DEFAULT_ERROR_MESSAGES['invalid'], 'on' => 'save', 'with' => null));
- foreach ($attrs as $attr)
- {
- $options = array_merge($configuration, $attr);
- $attribute = $options[0];
- $var = $this->model->$attribute;
- if (is_null($options['with']) || !is_string($options['with']) || !is_string($options['with']))
- throw new ValidationsArgumentError('A regular expression must be supplied as the [with] option of the configuration array.');
- else
- $expression = $options['with'];
- if ($this->is_null_with_option($var, $options) || $this->is_blank_with_option($var, $options))
- continue;
- if (!@preg_match($expression, $var))
- $this->record->add($attribute, $options['message']);
- }
- }
- /**
- * Validates the length of a value.
- *
- * <code>
- * class Person extends ActiveRecord\Model {
- * static $validates_length_of = array(
- * array('name', 'within' => array(1,50))
- * );
- * }
- * </code>
- *
- * Available options:
- *
- * <ul>
- * <li><b>is:</b> attribute should be exactly n characters long</li>
- * <li><b>in/within:</b> attribute should be within an range array(min,max)</li>
- * <li><b>maximum/minimum:</b> attribute should not be above/below respectively</li>
- * </ul>
- *
- * @param array $attrs Validation definition
- */
- public function validates_length_of($attrs)
- {
- $configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array(
- 'too_long' => Errors::$DEFAULT_ERROR_MESSAGES['too_long'],
- 'too_short' => Errors::$DEFAULT_ERROR_MESSAGES['too_short'],
- 'wrong_length' => Errors::$DEFAULT_ERROR_MESSAGES['wrong_length']
- ));
- foreach ($attrs as $attr)
- {
- $options = array_merge($configuration, $attr);
- $range_options = array_intersect(array_keys(self::$ALL_RANGE_OPTIONS), array_keys($attr));
- sort($range_options);
- switch (sizeof($range_options))
- {
- case 0:
- throw new ValidationsArgumentError('Range unspecified. Specify the [within], [maximum], or [is] option.');
- case 1:
- break;
- default:
- throw new ValidationsArgumentError('Too many range options specified. Choose only one.');
- }
- $attribute = $options[0];
- $var = $this->model->$attribute;
- $range_option = $range_options[0];
- if ($this->is_null_with_option($var, $options) || $this->is_blank_with_option($var, $options))
- continue;
- if ('within' == $range_option || 'in' == $range_option)
- {
- $range = $options[$range_options[0]];
- if (!(Utils::is_a('range', $range)))
- throw new ValidationsArgumentError("$range_option must be an array composing a range of numbers with key [0] being less than key [1]");
- if (is_float($range[0]) || is_float($range[1]))
- throw new ValidationsArgumentError("Range values cannot use floats for length.");
- if ((int)$range[0] <= 0 || (int)$range[1] <= 0)
- throw new ValidationsArgumentError("Range values cannot use signed integers.");
- $too_short = isset($options['message']) ? $options['message'] : $options['too_short'];
- $too_long = isset($options['message']) ? $options['message'] : $options['too_long'];
- $too_short = str_replace('%d', $range[0], $too_short);
- $too_long = str_replace('%d', $range[1], $too_long);
- if (strlen($this->model->$attribute) < (int)$range[0])
- $this->record->add($attribute, $too_short);
- elseif (strlen($this->model->$attribute) > (int)$range[1])
- $this->record->add($attribute, $too_long);
- }
- elseif ('is' == $range_option || 'minimum' == $range_option || 'maximum' == $range_option)
- {
- $option = $options[$range_option];
- if ((int)$option <= 0)
- throw new ValidationsArgumentError("$range_option value cannot use a signed integer.");
- if (is_float($option))
- throw new ValidationsArgumentError("$range_option value cannot use a float for length.");
- if (!is_null($this->model->$attribute))
- {
- $messageOptions = array('is' => 'wrong_length', 'minimum' => 'too_short', 'maximum' => 'too_long');
- if (isset($options[$messageOptions[$range_option]]))
- $message = $options[$messageOptions[$range_option]];
- else
- $message = $options['message'];
- $message = str_replace('%d', $option, $message);
- $attribute_value = $this->model->$attribute;
- $len = strlen($attribute_value);
- $value = (int)$attr[$range_option];
- if ('maximum' == $range_option && $len > $value)
- $this->record->add($attribute, $message);
- if ('minimum' == $range_option && $len < $value)
- $this->record->add($attribute, $message);
- if ('is' == $range_option && $len !== $value)
- $this->record->add($attribute, $message);
- }
- }
- }
- }
- /**
- * Validates the uniqueness of a value.
- *
- * <code>
- * class Person extends ActiveRecord\Model {
- * static $validates_uniqueness_of = array(
- * array('name'),
- * array(array('blah','bleh'), 'message' => 'blech')
- * );
- * }
- * </code>
- *
- * @param array $attrs Validation definition
- */
- public function validates_uniqueness_of($attrs)
- {
- $configuration = array_merge(self::$DEFAULT_VALIDATION_OPTIONS, array(
- 'message' => Errors::$DEFAULT_ERROR_MESSAGES['unique']
- ));
- foreach ($attrs as $attr)
- {
- $options = array_merge($configuration, $attr);
- $pk = $this->model->get_primary_key();
- $pk_value = $this->model->$pk[0];
- if (is_array($options[0]))
- {
- $add_record = join("_and_", $options[0]);
- $fields = $options[0];
- }
- else
- {
- $add_record = $options[0];
- $fields = array($options[0]);
- }
- $sql = "";
- $conditions = array("");
- if ($pk_value === null)
- $sql = "{$pk[0]} is not null";
- else
- {
- $sql = "{$pk[0]}!=?";
- array_push($conditions,$pk_value);
- }
- foreach ($fields as $field)
- {
- $field = $this->model->get_real_attribute_name($field);
- $sql .= " and {$field}=?";
- array_push($conditions,$this->model->$field);
- }
- $conditions[0] = $sql;
- if ($this->model->exists(array('conditions' => $conditions)))
- $this->record->add($add_record, $options['message']);
- }
- }
- private function is_null_with_option($var, &$options)
- {
- return (is_null($var) && (isset($options['allow_null']) && $options['allow_null']));
- }
- private function is_blank_with_option($var, &$options)
- {
- return (Utils::is_blank($var) && (isset($options['allow_blank']) && $options['allow_blank']));
- }
- }
- /**
- * Class that holds {@link Validations} errors.
- *
- * @package ActiveRecord
- */
- class Errors implements IteratorAggregate
- {
- private $model;
- private $errors;
- public static $DEFAULT_ERROR_MESSAGES = array(
- 'inclusion' => "is not included in the list",
- 'exclusion' => "is reserved",
- 'invalid' => "is invalid",
- 'confirmation' => "doesn't match confirmation",
- 'accepted' => "must be accepted",
- 'empty' => "can't be empty",
- 'blank' => "can't be blank",
- 'too_long' => "is too long (maximum is %d characters)",
- 'too_short' => "is too short (minimum is %d characters)",
- 'wrong_length' => "is the wrong length (should be %d characters)",
- 'taken' => "has already been taken",
- 'not_a_number' => "is not a number",
- 'greater_than' => "must be greater than %d",
- 'equal_to' => "must be equal to %d",
- 'less_than' => "must be less than %d",
- 'odd' => "must be odd",
- 'even' => "must be even",
- 'unique' => "must be unique",
- 'less_than_or_equal_to' => "must be less than or equal to %d",
- 'greater_than_or_equal_to' => "must be greater than or equal to %d"
- );
- /**
- * Constructs an {@link Errors} object.
- *
- * @param Model $model The model the error is for
- * @return Errors
- */
- public function __construct(Model $model)
- {
- $this->model = $model;
- }
- /**
- * Nulls $model so we don't get pesky circular references. $model is only needed during the
- * validation process and so can be safely cleared once that is done.
- */
- public function clear_model()
- {
- $this->model = null;
- }
- /**
- * Add an error message.
- *
- * @param string $attribute Name of an attribute on the model
- * @param string $msg The error message
- */
- public function add($attribute, $msg)
- {
- if (is_null($msg))
- $msg = self :: $DEFAULT_ERROR_MESSAGES['invalid'];
- if (!isset($this->errors[$attribute]))
- $this->errors[$attribute] = array($msg);
- else
- $this->errors[$attribute][] = $msg;
- }
- /**
- * Adds an error message only if the attribute value is {@link http://www.php.net/empty empty}.
- *
- * @param string $attribute Name of an attribute on the model
- * @param string $msg The error message
- */
- public function add_on_empty($attribute, $msg)
- {
- if (empty($msg))
- $msg = self::$DEFAULT_ERROR_MESSAGES['empty'];
- if (empty($this->model->$attribute))
- $this->add($attribute, $msg);
- }
- /**
- * Retrieve error message for an attribute.
- *
- * @param string $attribute Name of an attribute on the model
- * @return string
- */
- public function __get($attribute)
- {
- if (!isset($this->errors[$attribute]))
- return null;
- return $this->errors[$attribute];
- }
- /**
- * Adds the error message only if the attribute value was null or an empty string.
- *
- * @param string $attribute Name of an attribute on the model
- * @param string $msg The error message
- */
- public function add_on_blank($attribute, $msg)
- {
- if (!$msg)
- $msg = self::$DEFAULT_ERROR_MESSAGES['blank'];
- if (($value = $this->model->$attribute) === '' || $value === null)
- $this->add($attribute, $msg);
- }
- /**
- * Returns true if the specified attribute had any error messages.
- *
- * @param string $attribute Name of an attribute on the model
- * @return boolean
- */
- public function is_invalid($attribute)
- {
- return isset($this->errors[$attribute]);
- }
- /**
- * Returns the error message for the specified attribute or null if none.
- *
- * @param string $attribute Name of an attribute on the model
- * @return string
- */
- public function on($attribute)
- {
- if (!isset($this->errors[$attribute]))
- return null;
- $errors = $this->errors[$attribute];
- if (null === $errors)
- return null;
- else
- return count($errors) == 1 ? $errors[0] : $errors;
- }
- /**
- * Returns all the error messages as an array.
- *
- * <code>
- * $model->errors->full_messages();
- *
- * # array(
- * # "Name can't be blank",
- * # "State is the wrong length (should be 2 chars)"
- * # )
- * </code>
- *
- * @param array $options Options for messages
- * @return array
- */
- public function full_messages()
- {
- $full_messages = array();
- if ($this->errors)
- {
- foreach ($this->errors as $attribute => $messages)
- {
- foreach ($messages as $msg)
- {
- if (is_null($msg))
- continue;
- $full_messages[] = Utils::human_attribute($attribute) . ' ' . $msg;
- }
- }
- }
- return $full_messages;
- }
- /**
- * Returns true if there are no error messages.
- * @return boolean
- */
- public function is_empty()
- {
- return empty($this->errors);
- }
- /**
- * Clears out all error messages.
- */
- public function clear()
- {
- $this->errors = array();
- }
- /**
- * Returns the number of error messages there are.
- * @return int
- */
- public function size()
- {
- if ($this->is_empty())
- return 0;
- $count = 0;
- foreach ($this->errors as $attribute => $error)
- $count += count($error);
- return $count;
- }
- /**
- * Returns an iterator to the error messages.
- *
- * This will allow you to iterate over the {@link Errors} object using foreach.
- *
- * <code>
- * foreach ($model->errors as $msg)
- * echo "$msg\n";
- * </code>
- *
- * @return ArrayIterator
- */
- public function getIterator()
- {
- return new ArrayIterator($this->full_messages());
- }
- };
- ?>