post()) * ->label('email', 'E-mail') * ->rule('email', 'required') * ->rule('email', 'email') * ->rule('password', 'min_length', [8]) * ->rule('password_confirm', 'matches', ['password']); * * if ($validation->check()) { * // данные валидны * } else { * $errors = $validation->errors(); * } */ class Validation { /** @var array Проверяемые данные [поле => значение] */ private array $_data = []; /** @var array Правила [поле => [['rule' => ..., 'params' => [...]], ...]] */ private array $_rules = []; /** @var array Человекочитаемые названия полей [поле => подпись] */ private array $_labels = []; /** @var array Ошибки после check() [поле => сообщение] */ private array $_errors = []; /** @var array Шаблоны сообщений с плейсхолдерами :field/:param1/:param2 */ private static array $messages = [ 'required' => 'Поле «:field» обязательно для заполнения', 'email' => 'Поле «:field» должно быть корректным e-mail', 'url' => 'Поле «:field» должно быть корректным URL', 'numeric' => 'Поле «:field» должно быть числом', 'digit' => 'Поле «:field» должно содержать только цифры', 'min_length' => 'Поле «:field» должно быть не короче :param1 символов', 'max_length' => 'Поле «:field» должно быть не длиннее :param1 символов', 'exact_length' => 'Поле «:field» должно быть длиной :param1 символов', 'matches' => 'Поле «:field» должно совпадать с полем «:param1»', 'in' => 'Поле «:field» содержит недопустимое значение', 'regex' => 'Поле «:field» имеет неверный формат', ]; /** * @param array $data Проверяемые данные [поле => значение] */ private function __construct(array $data) { $this->_data = $data; } /** * Создаёт экземпляр валидатора над набором данных. * * @param array $data Проверяемые данные [поле => значение] * @return static */ public static function factory(array $data): static { return new static($data); } /** * Добавляет правило для поля. Правила одного поля проверяются по порядку, * до первой ошибки. * * @param string $field Имя поля * @param string $rule Имя правила (см. ключи self::$messages) * @param array $params Параметры правила (например [8] для min_length) * @return static */ public function rule(string $field, string $rule, array $params = []): static { $this->_rules[$field][] = ['rule' => $rule, 'params' => $params]; return $this; } /** * Задаёт человекочитаемую подпись поля для сообщений об ошибках. * * @param string $field Имя поля * @param string $label Подпись * @return static */ public function label(string $field, string $label): static { $this->_labels[$field] = $label; return $this; } /** * Прогоняет все правила. Заполняет список ошибок. * * @return bool true, если ошибок нет */ public function check(): bool { $this->_errors = []; foreach ($this->_rules as $field => $rules) { $value = $this->_data[$field] ?? null; foreach ($rules as $rule) { $name = $rule['rule']; $params = $rule['params']; // Пустое необязательное поле — остальные правила пропускаем. if ($name !== 'required' && self::isEmpty($value)) { continue; } if (!$this->execute($name, $value, $params)) { $this->_errors[$field] = $this->message($field, $name, $params); break; } } } return $this->_errors === []; } /** * Проверяет одно значение одним правилом. * * @param string $rule Имя правила * @param mixed $value Значение поля * @param array $params Параметры правила * @return bool * @throws MyException Если правило неизвестно */ private function execute(string $rule, mixed $value, array $params): bool { return match ($rule) { 'required' => !self::isEmpty($value), 'email' => filter_var($value, FILTER_VALIDATE_EMAIL) !== false, 'url' => filter_var($value, FILTER_VALIDATE_URL) !== false, 'numeric' => is_numeric($value), 'digit' => ctype_digit((string) $value), 'min_length' => mb_strlen((string) $value) >= (int) ($params[0] ?? 0), 'max_length' => mb_strlen((string) $value) <= (int) ($params[0] ?? 0), 'exact_length' => mb_strlen((string) $value) === (int) ($params[0] ?? 0), 'matches' => $value === ($this->_data[$params[0] ?? ''] ?? null), 'in' => in_array($value, (array) ($params[0] ?? []), true), 'regex' => preg_match($params[0] ?? '//', (string) $value) === 1, default => throw new MyException( 'Неизвестное правило валидации: :rule', [':rule' => $rule] ), }; } /** * Формирует сообщение об ошибке с подстановкой подписи поля и параметров. * * @param string $field Имя поля * @param string $rule Имя правила * @param array $params Параметры правила * @return string */ private function message(string $field, string $rule, array $params): string { $template = self::$messages[$rule] ?? 'Поле «:field» указано неверно'; // Для matches параметр — имя поля, показываем его подпись, если задана. $param1 = $params[0] ?? ''; if ($rule === 'matches') { $param1 = $this->_labels[$param1] ?? $param1; } return strtr($template, [ ':field' => $this->_labels[$field] ?? $field, ':param1' => is_scalar($param1) ? (string) $param1 : '', ':param2' => (string) ($params[1] ?? ''), ]); } /** * Возвращает все ошибки после check(). * * @return array [поле => сообщение] */ public function errors(): array { return $this->_errors; } /** * Возвращает сообщение об ошибке для одного поля. * * @param string $field Имя поля * @return string|null null, если ошибки нет */ public function error(string $field): ?string { return $this->_errors[$field] ?? null; } /** * Проверяет, считается ли значение пустым (null, '' или []). * * @param mixed $value Значение * @return bool */ private static function isEmpty(mixed $value): bool { return $value === null || $value === '' || $value === []; } }