358 lines
9.5 KiB
PHP
358 lines
9.5 KiB
PHP
<?php
|
|
|
|
namespace Web;
|
|
|
|
/**
|
|
* Validator provides input validation with chainable rules
|
|
*/
|
|
class Validator
|
|
{
|
|
private array $errors = [];
|
|
private array $data = [];
|
|
private array $rules = [];
|
|
private array $messages = [];
|
|
|
|
/**
|
|
* Custom validation rules registry
|
|
*/
|
|
private static array $customRules = [];
|
|
|
|
/**
|
|
* validate performs validation on data with given rules
|
|
*/
|
|
public function validate(array $data, array $rules, array $messages = []): bool
|
|
{
|
|
$this->data = $data;
|
|
$this->rules = $rules;
|
|
$this->messages = $messages;
|
|
$this->errors = [];
|
|
|
|
foreach ($rules as $field => $fieldRules) {
|
|
$this->validateField($field, $fieldRules);
|
|
}
|
|
|
|
return empty($this->errors);
|
|
}
|
|
|
|
/**
|
|
* validateField validates a single field against its rules
|
|
*/
|
|
private function validateField(string $field, string|array $rules): void
|
|
{
|
|
$rules = is_string($rules) ? explode('|', $rules) : $rules;
|
|
$value = $this->getValue($field);
|
|
|
|
foreach ($rules as $rule) {
|
|
$this->applyRule($field, $value, $rule);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* getValue gets a value from data using dot notation
|
|
*/
|
|
private function getValue(string $field): mixed
|
|
{
|
|
$keys = explode('.', $field);
|
|
$value = $this->data;
|
|
|
|
foreach ($keys as $key) {
|
|
if (!is_array($value) || !array_key_exists($key, $value)) {
|
|
return null;
|
|
}
|
|
$value = $value[$key];
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* applyRule applies a single validation rule
|
|
*/
|
|
private function applyRule(string $field, mixed $value, string $rule): void
|
|
{
|
|
$parts = explode(':', $rule, 2);
|
|
$ruleName = $parts[0];
|
|
$parameters = isset($parts[1]) ? explode(',', $parts[1]) : [];
|
|
|
|
$passes = match($ruleName) {
|
|
'required' => $this->validateRequired($value),
|
|
'email' => $this->validateEmail($value),
|
|
'url' => $this->validateUrl($value),
|
|
'alpha' => $this->validateAlpha($value),
|
|
'alphaNum' => $this->validateAlphaNum($value),
|
|
'numeric' => $this->validateNumeric($value),
|
|
'integer' => $this->validateInteger($value),
|
|
'float' => $this->validateFloat($value),
|
|
'boolean' => $this->validateBoolean($value),
|
|
'array' => $this->validateArray($value),
|
|
'json' => $this->validateJson($value),
|
|
'date' => $this->validateDate($value),
|
|
'min' => $this->validateMin($value, $parameters[0] ?? 0),
|
|
'max' => $this->validateMax($value, $parameters[0] ?? 0),
|
|
'between' => $this->validateBetween($value, $parameters[0] ?? 0, $parameters[1] ?? 0),
|
|
'length' => $this->validateLength($value, $parameters[0] ?? 0),
|
|
'in' => $this->validateIn($value, $parameters),
|
|
'notIn' => $this->validateNotIn($value, $parameters),
|
|
'regex' => $this->validateRegex($value, $parameters[0] ?? ''),
|
|
'confirmed' => $this->validateConfirmed($field, $value),
|
|
'unique' => $this->validateUnique($value, $parameters),
|
|
'exists' => $this->validateExists($value, $parameters),
|
|
default => $this->applyCustomRule($ruleName, $value, $parameters)
|
|
};
|
|
|
|
if (!$passes) {
|
|
$this->addError($field, $ruleName, $parameters);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validation rule methods
|
|
*/
|
|
private function validateRequired(mixed $value): bool
|
|
{
|
|
if ($value === null) return false;
|
|
if (is_string($value) && trim($value) === '') return false;
|
|
if (is_array($value) && empty($value)) return false;
|
|
return true;
|
|
}
|
|
|
|
private function validateEmail(mixed $value): bool
|
|
{
|
|
if (!is_string($value)) return false;
|
|
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
|
|
}
|
|
|
|
private function validateUrl(mixed $value): bool
|
|
{
|
|
if (!is_string($value)) return false;
|
|
return filter_var($value, FILTER_VALIDATE_URL) !== false;
|
|
}
|
|
|
|
private function validateAlpha(mixed $value): bool
|
|
{
|
|
if (!is_string($value)) return false;
|
|
return ctype_alpha($value);
|
|
}
|
|
|
|
private function validateAlphaNum(mixed $value): bool
|
|
{
|
|
if (!is_string($value)) return false;
|
|
return ctype_alnum($value);
|
|
}
|
|
|
|
private function validateNumeric(mixed $value): bool
|
|
{
|
|
return is_numeric($value);
|
|
}
|
|
|
|
private function validateInteger(mixed $value): bool
|
|
{
|
|
return filter_var($value, FILTER_VALIDATE_INT) !== false;
|
|
}
|
|
|
|
private function validateFloat(mixed $value): bool
|
|
{
|
|
return filter_var($value, FILTER_VALIDATE_FLOAT) !== false;
|
|
}
|
|
|
|
private function validateBoolean(mixed $value): bool
|
|
{
|
|
return in_array($value, [true, false, 0, 1, '0', '1', 'true', 'false'], true);
|
|
}
|
|
|
|
private function validateArray(mixed $value): bool
|
|
{
|
|
return is_array($value);
|
|
}
|
|
|
|
private function validateJson(mixed $value): bool
|
|
{
|
|
if (!is_string($value)) return false;
|
|
json_decode($value);
|
|
return json_last_error() === JSON_ERROR_NONE;
|
|
}
|
|
|
|
private function validateDate(mixed $value): bool
|
|
{
|
|
if (!is_string($value)) return false;
|
|
$date = date_parse($value);
|
|
return $date['error_count'] === 0 && $date['warning_count'] === 0;
|
|
}
|
|
|
|
private function validateMin(mixed $value, mixed $min): bool
|
|
{
|
|
if (is_numeric($value)) return $value >= $min;
|
|
if (is_string($value)) return strlen($value) >= $min;
|
|
if (is_array($value)) return count($value) >= $min;
|
|
return false;
|
|
}
|
|
|
|
private function validateMax(mixed $value, mixed $max): bool
|
|
{
|
|
if (is_numeric($value)) return $value <= $max;
|
|
if (is_string($value)) return strlen($value) <= $max;
|
|
if (is_array($value)) return count($value) <= $max;
|
|
return false;
|
|
}
|
|
|
|
private function validateBetween(mixed $value, mixed $min, mixed $max): bool
|
|
{
|
|
return $this->validateMin($value, $min) && $this->validateMax($value, $max);
|
|
}
|
|
|
|
private function validateLength(mixed $value, mixed $length): bool
|
|
{
|
|
if (is_string($value)) return strlen($value) == $length;
|
|
if (is_array($value)) return count($value) == $length;
|
|
return false;
|
|
}
|
|
|
|
private function validateIn(mixed $value, array $values): bool
|
|
{
|
|
return in_array($value, $values, true);
|
|
}
|
|
|
|
private function validateNotIn(mixed $value, array $values): bool
|
|
{
|
|
return !in_array($value, $values, true);
|
|
}
|
|
|
|
private function validateRegex(mixed $value, string $pattern): bool
|
|
{
|
|
if (!is_string($value)) return false;
|
|
return preg_match($pattern, $value) === 1;
|
|
}
|
|
|
|
private function validateConfirmed(string $field, mixed $value): bool
|
|
{
|
|
$confirmField = $field . '_confirmation';
|
|
return $value === $this->getValue($confirmField);
|
|
}
|
|
|
|
private function validateUnique(mixed $value, array $parameters): bool
|
|
{
|
|
// Placeholder for database uniqueness check
|
|
// Would require database connection in real implementation
|
|
return true;
|
|
}
|
|
|
|
private function validateExists(mixed $value, array $parameters): bool
|
|
{
|
|
// Placeholder for database existence check
|
|
// Would require database connection in real implementation
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* applyCustomRule applies a custom validation rule
|
|
*/
|
|
private function applyCustomRule(string $ruleName, mixed $value, array $parameters): bool
|
|
{
|
|
if (!isset(self::$customRules[$ruleName])) {
|
|
return true; // Unknown rules pass by default
|
|
}
|
|
|
|
return call_user_func(self::$customRules[$ruleName], $value, $parameters, $this->data);
|
|
}
|
|
|
|
/**
|
|
* addError adds a validation error
|
|
*/
|
|
private function addError(string $field, string $rule, array $parameters = []): void
|
|
{
|
|
$message = $this->messages["$field.$rule"]
|
|
?? $this->messages[$rule]
|
|
?? $this->getDefaultMessage($field, $rule, $parameters);
|
|
|
|
if (!isset($this->errors[$field])) {
|
|
$this->errors[$field] = [];
|
|
}
|
|
|
|
$this->errors[$field][] = $message;
|
|
}
|
|
|
|
/**
|
|
* getDefaultMessage gets a default error message
|
|
*/
|
|
private function getDefaultMessage(string $field, string $rule, array $parameters): string
|
|
{
|
|
$fieldName = str_replace('_', ' ', $field);
|
|
|
|
return match($rule) {
|
|
'required' => "The {$fieldName} field is required.",
|
|
'email' => "The {$fieldName} must be a valid email address.",
|
|
'url' => "The {$fieldName} must be a valid URL.",
|
|
'alpha' => "The {$fieldName} may only contain letters.",
|
|
'alphaNum' => "The {$fieldName} may only contain letters and numbers.",
|
|
'numeric' => "The {$fieldName} must be a number.",
|
|
'integer' => "The {$fieldName} must be an integer.",
|
|
'float' => "The {$fieldName} must be a float.",
|
|
'boolean' => "The {$fieldName} must be a boolean.",
|
|
'array' => "The {$fieldName} must be an array.",
|
|
'json' => "The {$fieldName} must be valid JSON.",
|
|
'date' => "The {$fieldName} must be a valid date.",
|
|
'min' => "The {$fieldName} must be at least {$parameters[0]}.",
|
|
'max' => "The {$fieldName} must not be greater than {$parameters[0]}.",
|
|
'between' => "The {$fieldName} must be between {$parameters[0]} and {$parameters[1]}.",
|
|
'length' => "The {$fieldName} must be exactly {$parameters[0]} characters.",
|
|
'in' => "The selected {$fieldName} is invalid.",
|
|
'notIn' => "The selected {$fieldName} is invalid.",
|
|
'regex' => "The {$fieldName} format is invalid.",
|
|
'confirmed' => "The {$fieldName} confirmation does not match.",
|
|
'unique' => "The {$fieldName} has already been taken.",
|
|
'exists' => "The selected {$fieldName} is invalid.",
|
|
default => "The {$fieldName} is invalid."
|
|
};
|
|
}
|
|
|
|
/**
|
|
* errors returns all validation errors
|
|
*/
|
|
public function errors(): array
|
|
{
|
|
return $this->errors;
|
|
}
|
|
|
|
/**
|
|
* failed checks if validation failed
|
|
*/
|
|
public function failed(): bool
|
|
{
|
|
return !empty($this->errors);
|
|
}
|
|
|
|
/**
|
|
* passed checks if validation passed
|
|
*/
|
|
public function passed(): bool
|
|
{
|
|
return empty($this->errors);
|
|
}
|
|
|
|
/**
|
|
* firstError gets the first error message
|
|
*/
|
|
public function firstError(string $field = null): ?string
|
|
{
|
|
if ($field) {
|
|
return $this->errors[$field][0] ?? null;
|
|
}
|
|
|
|
foreach ($this->errors as $errors) {
|
|
if (!empty($errors)) {
|
|
return $errors[0];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* extend adds a custom validation rule
|
|
*/
|
|
public static function extend(string $name, callable $callback): void
|
|
{
|
|
self::$customRules[$name] = $callback;
|
|
}
|
|
}
|