Валидация
Валидатор Lift отвечает на один вопрос: «соответствуют ли эти входные данные ожидаемым мной правилам?» — и даёт точный список того, что не прошло.
Он работает с любым ассоциативным массивом: тело HTTP-запроса, строка запроса, параметры JSON-RPC, аргументы CLI, даже строка, прочитанная из другого сервиса. DSL намеренно похож на Laravel, поэтому кривая обучения почти нулевая.
Ментальная модель: вы описываете каждое поле списком правил (
'required|email|max:255'). Валидатор собирает все ошибки (он не останавливается на первой) и возвращает либо очищенные данные, либо карту ошибок.
1. Тур за 60 секунд
use Lift\Validation\Validator;
$v = new Validator($_POST, [
'name' => 'required|string|min:2|max:255',
'email' => 'required|email',
'age' => 'integer|min:13|max:120',
'role' => 'required|in:admin,user,moderator',
'website' => 'nullable|url',
]);
if ($v->fails()) {
return Response::json(['errors' => $v->errors()], 422);
}
$data = $v->validated();
Три вещи, которые нужно помнить:
- Правила могут быть строкой через вертикальную черту (
'required|email') или массивом правил/объектов/замыканий (['required', 'email', new MyRule()]). $v->errors()— этоarray<string, string[]>— у каждого поля может быть несколько сообщений об ошибках.$v->validated()возвращает только те поля, для которых вы объявили правила (чистый DTO).
2. Валидация внутри маршрута
В HTTP-обработчике однострочник $req->validate(...) — самый простой путь. Он объединяет тело + query + параметры маршрута, запускает валидатор, выбрасывает ValidationException при ошибке, а иначе возвращает валидированный массив. Обработчик ошибок Lift по умолчанию преобразует исключение в HTTP 422 с правильной формой JSON — try/catch писать не нужно:
$app->post('/users', function (Request $req) use ($repo) {
$data = $req->validate([
'name' => 'required|string|min:2',
'email' => 'required|email',
'password' => 'required|min:8|confirmed',
]);
return Response::json($repo->create($data), 201);
});
Тело ответа при ошибке выглядит так:
{
"errors": {
"email": ["The email must be a valid email address."],
"password": ["The password must be at least 8 characters."]
}
}
Для типизированного, переиспользуемого контейнера используйте FormRequest.
3. Шпаргалка по возвращаемым значениям
| Метод | Возвращает | Примечания |
|---|---|---|
passes() |
bool |
true, когда проходят все правила |
fails() |
bool |
!passes() |
errors() |
array<string, string[]> |
поле → список сообщений |
validated() |
array<string, mixed> |
выбрасывает ValidationException при ошибке |
4. Синтаксис правил
// Через вертикальную черту (компактно, рекомендуется для простых случаев)
'email' => 'required|email|max:255'
// Массив (позволяет смешивать замыкания и объекты правил)
'phone' => ['required', 'string', new PhoneRule()]
Правила выполняются в том порядке, в котором вы их перечислили. required, nullable и sometimes особенные — они влияют на то, выполняется ли остальная цепочка вообще (см. §6 ниже).
Несколько ошибок на поле собираются: валидация не останавливается на первой неудаче, поэтому пользователь видит все проблемы сразу.
5. Встроенные правила — полный справочник
Присутствие и поток
| Правило | Описание |
|---|---|
required |
Поле должно присутствовать и быть непустым ('', [], null все не проходят). |
nullable |
Если поле отсутствует / null / пустое, пропустить остаток цепочки. |
sometimes |
Если ключа вообще нет во входных данных, пропустить все правила. Отлично для PATCH. |
present |
Ключ должен существовать (значение может быть null или ''). |
filled |
Если ключ существует, значение не должно быть пустым. |
'bio' => 'nullable|string|max:500', // пустая строка допустима
'avatar_url' => 'sometimes|url', // может отсутствовать при PATCH
'profile.id' => 'present', // должен присутствовать, даже как null
Условное required / prohibited
Ссылайтесь на любое другое поле через точечный путь. Работают и поля верхнего уровня, и вложенные.
| Правило | Описание |
|---|---|
required_if:field,value |
Обязательно, когда field равно value. |
required_unless:field,value |
Обязательно, если только field не равно value. |
required_with:f1,f2,... |
Обязательно, если любое из перечисленных полей непустое. |
required_without:f1,f2,... |
Обязательно, если любое из перечисленных полей отсутствует/пустое. |
prohibited |
Поле должно отсутствовать или быть пустым. |
prohibited_if:field,value |
Запрещено, когда field равно value. |
prohibited_unless:field,value |
Запрещено, если только field не равно value. |
'type' => 'required|in:individual,company',
'company.name' => 'required_if:type,company|string|max:200',
'person.dob' => 'required_unless:type,company|date',
'admin_token' => 'prohibited_unless:role,admin|string',
'password' => 'prohibited_if:role,guest|string|min:8',
Тип
| Правило | Проходит, когда |
|---|---|
string |
Значение — строка PHP. |
integer / int |
Числовое целое (принимает "42"). |
float / numeric |
Числовое (int или float). |
boolean / bool |
Одно из true, false, 1, 0, "1", "0", "true", "false". |
array |
Массив PHP. |
Формат
| Правило | Проходит, когда |
|---|---|
email |
Корректный адрес электронной почты. |
url |
Корректный URL. |
ip / ipv4 / ipv6 |
Соответствующий IP-адрес. |
alpha |
Только ASCII-буквы. |
alpha_num |
Только ASCII-буквы + цифры. |
digits |
Только цифровые символы. |
digits_between:min,max |
Только цифры, длина между min и max. |
date |
Разбирается функцией strtotime(). |
date_format:fmt |
Соответствует заданному формату даты PHP (например, Y-m-d). |
json |
Корректная строка JSON. |
uuid |
Корректный UUID v1–v5. |
mac_address |
AA:BB:CC:DD:EE:FF (двоеточия или дефисы). |
regex:/pattern/ |
Соответствует regex. |
not_regex:/pattern/ |
Не соответствует regex. |
lowercase / uppercase |
Вся строка в нижнем/верхнем регистре. |
Ограничения значения
| Правило | Проходит, когда |
|---|---|
min:n / max:n |
Число ≥/≤ n; длина строки ≥/≤ n; количество в массиве ≥/≤ n. |
between:min,max |
Числовое значение между min и max (включительно). |
size:n |
Точное значение / длина строки / количество в массиве. |
min_length:n / max_length:n |
Длина строки (независимо от числового содержимого). |
multiple_of:n |
Число делится на n нацело. |
in:a,b,c |
Значение — один из перечисленных вариантов. |
not_in:a,b,c |
Значение не входит в перечисленные варианты. |
accepted / declined |
Одно из yes/on/1/true (или no/off/0/false). |
confirmed |
Соседнее поле {name}_confirmation существует и равно. |
same:other / different:other |
Значение равно / отличается от другого поля. |
starts_with:pfx / ends_with:sfx |
Строка начинается/заканчивается заданной подстрокой. |
Правила для массивов
| Правило | Проходит, когда |
|---|---|
list |
Ключи — 0, 1, 2, … (без строковых ключей, без пропусков). |
distinct |
Все значения массива уникальны. |
min_items:n |
В массиве не менее n элементов. |
max_items:n |
В массиве не более n элементов. |
6. required, nullable, sometimes — когда что происходит?
Самые тонкие правила системы. Запомните эту таблицу:
| Состояние ввода | required |
nullable |
sometimes |
|---|---|---|---|
| Ключ полностью отсутствует | ❌ не проходит | пропустить остаток | пропустить всё |
Ключ есть, значение null / '' / [] |
❌ не проходит | пропустить остаток | выполнить остальные правила |
| Ключ есть, реальное значение | выполнить правила | выполнить правила | выполнить правила |
Простыми словами:
nullable— «это поле можно оставить пустым / null, но если оно заполнено, оно должно удовлетворять правилам».sometimes— «это поле может вообще отсутствовать во входных данных; если оно есть, валидировать как обычно». Идеально для PATCH-эндпоинтов.required— «это поле должно присутствовать и быть непустым».
7. Вложенные данные с точечными путями
$v = new Validator($data, [
'user.name' => 'required|string|max:100',
'user.email' => 'required|email',
'user.address.city' => 'required|string',
'user.address.zip' => 'required|digits_between:5,10',
'user.preferences.lang' => 'required|in:en,ru,de,fr',
]);
Ошибки имеют ключ с тем же точечным путём:
{ "errors": { "user.address.zip": ["The user.address.zip must be 5-10 digits."] } }
8. Подстановочные знаки (.*) — валидация массивов сущностей
.* раскрывается в каждый элемент с целочисленным индексом родительского массива.
$v = new Validator($data, [
'tags' => 'required|array|list|distinct|min_items:1|max_items:10',
'tags.*' => 'required|string|max:50|alpha_num',
]);
Ключи ошибок становятся tags.0, tags.1, … так что фронтенд может сопоставить ошибки с нужным <input>.
Вложенные подстановочные знаки (массив объектов):
$v = new Validator($data, [
'items' => 'required|array|min_items:1|max_items:100',
'items.*.name' => 'required|string|max:200',
'items.*.sku' => 'required|string|regex:/^[A-Z0-9\-]+$/',
'items.*.qty' => 'required|integer|min:1',
'items.*.tags' => 'nullable|array|list|max_items:10',
'items.*.tags.*' => 'string|max:50',
]);
9. Правила-замыкания — быстрая встроенная логика
Замыкание получает ($field, $value, $allData, $fail). Вызовите $fail("message"), чтобы отметить правило проваленным:
$v = new Validator($data, [
'slug' => [
'required', 'string', 'min:3',
function (string $field, mixed $value, array $data, \Closure $fail): void {
if (str_contains($value, '--')) {
$fail("The {$field} must not contain consecutive hyphens.");
}
},
],
]);
Замыкание получает все данные — идеально для межполевых проверок ('end_date' >= 'start_date' и т. п.).
10. Переиспользуемые классы правил (RuleInterface)
Для логики, которую вы переиспользуете в 3+ местах, оформите её как класс:
use Lift\Validation\RuleInterface;
final class PhoneRule implements RuleInterface
{
public function passes(string $field, mixed $value, array $data): bool
{
return is_string($value) && preg_match('/^\+?[0-9]{10,15}$/', $value) === 1;
}
public function message(): string
{
return 'The :attribute must be a valid phone number.';
}
}
$v = new Validator($data, [
'phone' => ['required', new PhoneRule()],
]);
Плейсхолдер :attribute автоматически заменяется именем поля. Переопределите его для конкретного поля через массив собственных сообщений (следующий раздел).
11. Собственные сообщения об ошибках
Передайте массив третьим аргументом конструктора. Ключи — "field.rule" (наиболее конкретный) или просто "rule" (запасной вариант для всего правила). Плейсхолдеры :attribute, :min, :max, :value, :other, :when, :values подставляются автоматически.
$v = new Validator($data, $rules, [
// На поле
'email.required' => 'We need your email address.',
'email.email' => ':attribute does not look right.',
// Запасной вариант для всех полей, использующих правило
'required' => 'This field is required.',
'min' => ':attribute must be at least :min.',
]);
Внутри FormRequest переопределите messages():
public function messages(): array
{
return [
'password.min' => 'Password must be at least :min characters.',
];
}
12. Регистрация собственных правил глобально
Для правил, которые должны быть доступны везде ('card' => 'required|luhn'):
use Lift\Validation\Validator;
// Форма-замыкание
Validator::extend(
'luhn',
fn(string $field, mixed $value, array $data) => $this->checkLuhn($value),
'The :attribute must be a valid card number.',
);
// Форма RuleInterface (использует собственный message())
Validator::extend('isbn13', new Isbn13Rule());
Регистрируйте при загрузке (например, в public/index.php или файле начальной загрузки).
13. Реальный пример — заказ в интернет-магазине
$data = $req->validate([
// Заголовок заказа
'currency' => 'required|string|size:3|uppercase',
'coupon_code' => 'nullable|string|max:30|alpha_num',
'note' => 'nullable|string|max:1000',
// Доставка
'shipping.name' => 'required|string|max:100',
'shipping.line1' => 'required|string|max:200',
'shipping.line2' => 'nullable|string|max:200',
'shipping.city' => 'required|string|max:100',
'shipping.zip' => 'required|digits_between:4,10',
'shipping.country_code' => 'required|alpha|max:2|uppercase',
// Позиции — 1..50
'items' => 'required|array|list|min_items:1|max_items:50',
'items.*.product_id' => 'required|uuid',
'items.*.qty' => 'required|integer|min:1|max:999',
'items.*.unit_price' => 'required|numeric|min:0',
'items.*.promotions' => 'nullable|array|list|max_items:5',
'items.*.promotions.*' => 'string|max:50',
// Оплата
'payment.method' => 'required|in:card,paypal,bank_transfer',
'payment.token' => 'required_if:payment.method,card|string',
'payment.paypal_email' => 'required_if:payment.method,paypal|email',
'payment.bank_reference' => 'required_if:payment.method,bank_transfer|string|max:100',
'payment.save_card' => 'prohibited_unless:payment.method,card|boolean',
]);
14. Локализованные сообщения об ошибках
Передайте Translator для вывода не на английском:
use Lift\Translation\Translator;
// Глобальное значение по умолчанию
Validator::setTranslator(new Translator('ru'));
// Или на экземпляр
$v = new Validator($data, $rules, [], new Translator('fr'));
Файл переводов использует ключи сообщений вроде validation.required, validation.email и т. д. Формат см. в Локализации.
15. ValidationException — программное использование
Когда нужно провалить валидацию извне валидатора (например, после запроса к БД):
use Lift\Validation\ValidationException;
throw ValidationException::withErrors([
'email' => ['This email is already registered.'],
]);
Обработчик ошибок Lift преобразует его в HTTP 422 так же, как любую другую неудачу валидации. Чтобы перехватить и осмотреть его:
try {
$data = $v->validated();
} catch (ValidationException $e) {
$errors = $e->errors(); // ['field' => ['msg', …], …]
}
Частые подводные камни
| Симптом | Причина | Исправление |
|---|---|---|
Все необязательные поля проваливаются с required |
Вы поставили nullable после правил, которые уже провалились |
Ставьте nullable первым: 'nullable|string|max:50'. |
nullable не помогает, когда ключ отсутствует |
nullable обрабатывает только пустые значения, не отсутствующие ключи |
Используйте sometimes для «может вообще отсутствовать». |
| Подстановочный знак валидирует и строковые ключи | .* раскрывает только элементы с целочисленным индексом |
Добавьте array|list на родителя, чтобы сначала обеспечить форму списка. |
min:5 отклонил '12' (строку длины 2) |
min трактует числовые строки как числа |
Используйте min_length:5 для явной проверки длины строки. |
confirmed не срабатывает |
Соседнее поле должно быть точно {name}_confirmation |
Проверьте написание — password → password_confirmation. |
| Собственное правило никогда не выполняется | Вы добавили его в замыкание, которое возвращает значение вместо вызова $fail() |
Замыкания должны вызывать $fail(...) при ошибке, а не возвращать false. |
| Все ошибки говорят «The X field is invalid» | Нет собственных сообщений, откат к общему шаблону | Добавьте сообщения или используйте глобальный переводчик. |
Шпаргалка
// Самое частое: однострочник внутри обработчика
$data = $req->validate([
'email' => 'required|email',
'age' => 'integer|min:13',
]);
// Отдельно
$v = new Validator($input, $rules, $customMessages = []);
$v->passes() / $v->fails() / $v->errors() / $v->validated();
// Собственное правило
final class FooRule implements RuleInterface { … }
Validator::extend('foo', new FooRule());
// Выбросить своё
throw ValidationException::withErrors(['email' => ['already taken']]);