219 lines
8.4 KiB
PHP
219 lines
8.4 KiB
PHP
<?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 === [];
|
||
}
|
||
}
|