Bicycle/System/Classes/Validation.php
Egor Isaev e6e8844f2b dev
2026-06-23 14:17:26 +03:00

219 lines
8.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* @package Bicycle
* @author Egor Isaev
* @description Validation.php
* @copyright (c) 23/06/2026
*/
namespace System\Classes;
/**
* Валидация входных данных по декларативным правилам.
* Источник данных — обычно Request::post()/query().
*
* Использование:
* $validation = Validation::factory($request->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<string,string> Шаблоны сообщений с плейсхолдерами :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 === [];
}
}