Условная валидация в Laravel
Формы редко бывают линейными. Какие поля обязательны, какие вообще нужны, а какие запрещены – зависит от того, что пользователь выбрал в соседнем поле. Тип оплаты определяет, нужен ли номер карты. Тип клиента – физлицо или юрлицо – меняет половину формы. Без условной валидации пришлось бы писать if-ветвления руками, дублируя правила и теряя декларативность.
Laravel покрывает эти сценарии набором правил: от строковых required_if и exclude_unless до fluent-методов Rule::requiredIf() и метода $validator->sometimes() для произвольной логики. Разберём каждый инструмент, посмотрим, когда какой выбрать, и обойдём грабли, на которые наступают чаще всего.
required_if – обязательно при условии
Правило required_if делает поле обязательным, когда другое поле равно заданному значению. Типичная ситуация – форма размещения объявления, где набор полей зависит от типа сделки:
$request->validate([
'deal_type' => 'required|in:sale,rent',
'price' => 'required|integer|min:1',
'rent_period' => 'required_if:deal_type,rent|in:month,year',
'cadastral_number' => 'required_if:deal_type,sale|string|size:13',
]);
Если deal_type равен rent, поле rent_period станет обязательным. При sale – понадобится cadastral_number.
Можно перечислить несколько значений через запятую:
'tax_id' => 'required_if:entity_type,company,sole_proprietor|string',
Поле tax_id обязательно, когда entity_type равно company или sole_proprietor.
Rule::requiredIf() для сложных условий
Строковый синтаксис ограничен сравнением с конкретными значениями. Когда условие сложнее – передайте замыкание в Rule::requiredIf():
use Illuminate\Validation\Rule;
$request->validate([
'guarantor_name' => [
Rule::requiredIf(fn () => $request->integer('loan_amount') > 500_000),
'string',
'max:255',
],
]);
Поручитель нужен только при сумме кредита выше 500 000. Замыкание возвращает true или false, никакой привязки к конкретному полю.
Можно передать и просто boolean:
'admin_code' => Rule::requiredIf($user->isManager()),
required_unless – обязательно, если НЕ равно
Зеркало required_if. Поле обязательно, пока другое поле не равно одному из перечисленных значений:
$request->validate([
'role' => 'required|in:admin,editor,viewer',
'department' => 'required_unless:role,admin|string',
]);
Администратору отдел не нужен – у него доступ ко всему. Остальным – обязательно.
Особый случай – значение null: required_unless:name,null сделает поле обязательным, если name не равно null и не отсутствует в запросе. Это удобно для полей, которые могут быть сброшены.
Rule::requiredUnless() работает аналогично Rule::requiredIf(), только с обратной логикой:
'two_factor_code' => Rule::requiredUnless(fn () => '192.168.1.10' === $request->ip()),
required_if_accepted и required_if_declined
Пара правил для работы с чекбоксами и тогглами. required_if_accepted делает поле обязательным, когда другое поле имеет значение "yes", "on", 1, "1", true или "true":
$request->validate([
'subscribe_newsletter' => 'boolean',
'preferred_frequency' => 'required_if_accepted:subscribe_newsletter|in:daily,weekly,monthly',
]);
Подписался на рассылку – выбери периодичность.
required_if_declined – зеркало, срабатывает на "no", "off", 0, "0", false, "false":
$request->validate([
'use_default_address' => 'required|boolean',
'custom_address' => 'required_if_declined:use_default_address|string|max:500',
]);
Отказался от адреса по умолчанию – укажи свой. Эти правила удобнее required_if:field,true тем, что принимают все вариации «да/нет», которые приходят из разных типов форм.
required_with и required_without
required_with – обязательно при наличии другого поля
Когда пользователь начинает заполнять блок, все поля блока должны быть заполнены. required_with делает поле обязательным, если хотя бы одно из перечисленных полей присутствует и непустое:
$request->validate([
'street' => 'nullable|string|max:255',
'city' => 'required_with:street|string|max:100',
'zip_code' => 'required_with:street|string|max:10',
]);
Указал улицу – укажи и город с индексом. Оставил всё пустым – валидация пройдёт.
required_with_all строже: поле обязательно, только если все перечисленные поля заполнены:
'shipping_notes' => 'required_with_all:street,city,zip_code|string',
required_without – обязательно при отсутствии
Обратная ситуация. Поле обязательно, когда другое поле не заполнено:
$request->validate([
'phone' => 'required_without:email|string|max:20',
'email' => 'required_without:phone|email',
]);
Пользователь должен оставить хотя бы один контакт – телефон или email. Оба тоже можно.
required_without_all – поле обязательно, только если ни одно из перечисленных полей не заполнено:
$request->validate([
'phone' => 'nullable|string',
'email' => 'nullable|email',
'telegram' => 'required_without_all:phone,email|string',
]);
Если не указаны ни телефон, ни email – Telegram обязателен.
Одно из двух полей обязательно
Частый вопрос: как сделать, чтобы пользователь заполнил ровно одно из двух полей? required_without решает «хотя бы одно», но не запрещает оба одновременно.
Для взаимоисключения подходит prohibits – если поле заполнено, указанные поля должны быть пустыми:
$request->validate([
'pickup_point_id' => 'nullable|integer|prohibits:delivery_address',
'delivery_address' => 'nullable|string|prohibits:pickup_point_id',
]);
Выбрал пункт выдачи – адрес доставки запрещён, и наоборот.
Ещё один подход к взаимоисключению – XOR через кастомное правило или after():
public function after(): array
{
return [
function (\Illuminate\Validation\Validator $validator) {
$hasPhone = filled($this->input('phone'));
$hasEmail = filled($this->input('email'));
if ($hasPhone && $hasEmail) {
$validator->errors()->add('phone', 'Укажите что-то одно: телефон или email.');
}
if (!$hasPhone && !$hasEmail) {
$validator->errors()->add('phone', 'Укажите хотя бы телефон или email.');
}
},
];
}
Этот вариант гибче: здесь уже не «хотя бы одно», а именно «ровно одно». Декларативными правилами такое выразить сложно, after() справляется проще.
exclude_if и exclude_unless – убрать поле из валидации
required_if делает поле обязательным, но не убирает его из данных, если условие не выполнено. Это ключевое отличие от exclude_if: правило exclude_if полностью исключает поле из результата $request->validated().
Практическая разница: если поле с required_if получит значение, оно попадёт в валидированные данные независимо от условия. С exclude_if поле физически отсутствует.
$request->validate([
'has_promo' => 'required|boolean',
'promo_code' => 'exclude_if:has_promo,false|required|string|size:8',
]);
$validated = $request->validated();
// Если has_promo = false, ключа promo_code в $validated нет вообще
Это удобно при массовом заполнении модели – лишние поля не попадут в $model->fill($validated).
exclude_unless – зеркальная логика:
$request->validate([
'delivery_type' => 'required|in:pickup,courier',
'address' => 'exclude_unless:delivery_type,courier|required|string',
'pickup_point' => 'exclude_unless:delivery_type,pickup|required|integer|exists:pickup_points,id',
]);
Адрес валидируется и попадает в данные только при курьерской доставке. При pickup ключ address не появится в массиве $request->validated(), даже если фронтенд случайно отправил его – exclude_unless вырежет поле до того, как данные дойдут до контроллера.
Как и в required_unless, здесь работает специальное значение null: exclude_unless:name,null исключит поле, если name не равно null и присутствует в запросе.
Rule::excludeIf() с замыканием
use Illuminate\Validation\Rule;
$request->validate([
'spouse_name' => [
Rule::excludeIf(fn () => 'single' === $request->input('marital_status')),
'required',
'string',
'max:255',
],
]);
exclude_with и exclude_without
Более простые формы. exclude_with убирает поле, если другое поле присутствует:
'manual_total' => 'exclude_with:auto_calculate|required|numeric',
exclude_without убирает поле, если другое поле отсутствует:
'shipping_insurance' => 'exclude_without:shipping_address|boolean',
prohibited_if и prohibited_unless
Если exclude убирает поле из данных молча, то prohibited выбрасывает ошибку валидации, когда поле заполнено, но не должно быть:
$request->validate([
'account_type' => 'required|in:free,premium',
'custom_domain' => 'prohibited_if:account_type,free|string|max:255',
]);
Пользователь на бесплатном плане пытается указать кастомный домен – получит ошибку, а не молчаливое игнорирование.
prohibited_unless работает наоборот – поле запрещено, если другое поле не равно значению:
'api_key' => 'prohibited_unless:account_type,premium|string|size:32',
API-ключ разрешён только для premium-аккаунтов.
Для сложной логики – Rule::prohibitedIf() и Rule::prohibitedUnless():
'beta_feature' => Rule::prohibitedIf(fn () => !$user->isBetaTester()),
prohibits – взаимное исключение
prohibits запрещает перечисленные поля, если текущее поле заполнено:
$request->validate([
'is_anonymous' => 'boolean|prohibits:display_name,avatar',
'display_name' => 'nullable|string|max:50',
'avatar' => 'nullable|image|max:2048',
]);
Отметил анонимность – имя и аватар должны быть пустыми.
sometimes – валидировать только при наличии поля
sometimes – самостоятельный инструмент условной валидации: правила применяются, только если ключ реально присутствует в данных запроса.
$validator = Validator::make($data, [
'phone' => 'sometimes|required|string|min:10',
]);
Если ключа phone нет в $data, валидатор пропустит его целиком. Если есть – дальше работает required, и пустая строка не пройдёт.
Главный сценарий – PATCH-обновления, где фронтенд отправляет только изменённые поля:
// UpdateProfileRequest
public function rules(): array
{
return [
'name' => 'sometimes|string|max:255',
'email' => 'sometimes|email|unique:users,email,' . $this->user()->id,
'bio' => 'sometimes|nullable|string|max:1000',
];
}
Подробное описание sometimes вместе с родственными filled, present, missing – в статье про правила валидации.
nullable vs sometimes – в чём разница
Эта пара путается чаще всего именно в условных сценариях:
sometimes– условие на уровне присутствия ключа. Нет ключа – нет проверкиnullable– условие на уровне значения. Ключ есть, значениеnull– ok
// Поле можно не отправлять. Если отправили – проверяем формат
'bio' => 'sometimes|nullable|string|max:1000',
// Поле обязательно, но может быть null
'supervisor_id' => 'required|nullable|integer|exists:users,id',
На PATCH-запросах нужны оба: sometimes разрешает не отправлять поле, nullable – отправить пустое значение. Middleware ConvertEmptyStringsToNull (включён по умолчанию) превращает пустые строки в null, поэтому nullable обычно идёт вместе с sometimes.
Значения по умолчанию
Laravel не имеет встроенного правила default. Вместо этого используют prepareForValidation() в Form Request:
use Illuminate\Foundation\Http\FormRequest;
class CreateOrderRequest extends FormRequest
{
protected function prepareForValidation(): void
{
$this->mergeIfMissing([
'currency' => 'RUB',
'locale' => app()->getLocale(),
'notify' => true,
]);
}
public function rules(): array
{
return [
'currency' => 'required|string|in:RUB,USD,EUR',
'locale' => 'required|string|size:2',
'notify' => 'required|boolean',
];
}
}
mergeIfMissing() подставляет значение, только если ключ отсутствует в данных запроса. Если ключ есть, но со значением null – подстановка не произойдёт. Поле дальше проходит валидацию на общих основаниях.
Для замены null на конкретное значение – проверьте вручную:
protected function prepareForValidation(): void
{
if (null === $this->input('priority')) {
$this->merge(['priority' => 'normal']);
}
}
Подробнее о Form Request – в отдельной статье.
Условные правила в Form Request
В Form Request условную логику можно строить прямо в методе rules() через обычный PHP:
class UpdateProductRequest extends FormRequest
{
public function rules(): array
{
$rules = [
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'category_id' => 'required|exists:categories,id',
];
if ('PUT' === $this->method()) {
// Полное обновление – все поля обязательны
$rules['description'] = 'required|string|max:5000';
$rules['sku'] = 'required|string|unique:products,sku,' . $this->route('product');
} else {
// PATCH – частичное обновление
$rules = collect($rules)
->map(fn ($rule) => 'sometimes|' . $rule)
->all();
}
return $rules;
}
}
Такой подход позволяет менять набор правил в зависимости от HTTP-метода, роли пользователя, состояния модели – любых данных, доступных в объекте Request.
Другой распространённый паттерн – after() для пост-валидационных проверок, зависящих от комбинации уже валидированных полей:
use Illuminate\Validation\Validator;
class BookingRequest extends FormRequest
{
public function rules(): array
{
return [
'check_in' => 'required|date|after:today',
'check_out' => 'required|date|after:check_in',
'guests' => 'required|integer|min:1|max:10',
'room_type' => 'required|in:standard,suite,penthouse',
];
}
public function after(): array
{
return [
function (Validator $validator) {
$guests = $this->integer('guests');
$roomType = $this->input('room_type');
if ($guests > 4 && 'standard' === $roomType) {
$validator->errors()->add(
'room_type',
'Стандартный номер вмещает не более 4 гостей.'
);
}
},
];
}
}
Метод after() возвращает массив замыканий, которые выполняются после основной валидации. Внутри доступен объект Validator – можно добавить ошибки к конкретным полям. Это полезно, когда условие зависит от нескольких полей сразу и не укладывается в декларативные правила. Подробнее о правилах date|after и других временных ограничениях – в статье про валидацию дат.
Условная валидация с enum
Правило Rule::enum() поддерживает метод when() для динамического ограничения допустимых значений:
use App\Enums\OrderStatus;
use Illuminate\Validation\Rule;
$request->validate([
'status' => [
'required',
Rule::enum(OrderStatus::class)
->when(
$user->isManager(),
fn ($rule) => $rule->only(OrderStatus::Approved, OrderStatus::Rejected),
fn ($rule) => $rule->except(OrderStatus::Approved, OrderStatus::Rejected),
),
],
]);
Менеджер может ставить статусы Approved и Rejected. Обычный пользователь – все остальные, кроме этих двух.
Метод when() принимает три аргумента: условие, замыкание для true, замыкание для false (необязательный). Третий аргумент можно опустить, если при невыполненном условии правило не нужно менять.
Условие может быть замыканием:
Rule::enum(Priority::class)
->when(
fn () => $request->boolean('is_urgent'),
fn ($rule) => $rule->only(Priority::High, Priority::Critical),
);
Enum в сочетании с required_if тоже работает:
$request->validate([
'action' => 'required|in:approve,reject,hold',
'reject_reason' => [
'required_if:action,reject',
Rule::enum(RejectReason::class),
],
]);
$validator->sometimes() – произвольная условная логика
Строковые правила привязаны к значению другого поля. Когда условие зависит от комбинации полей, внешних данных или вычислений – используйте метод sometimes() на экземпляре валидатора:
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Fluent;
$validator = Validator::make($request->all(), [
'weight' => 'required|numeric|min:0.1',
'destination_country' => 'required|string|size:2',
'shipping_method' => 'required|in:standard,express,freight',
]);
$validator->sometimes('customs_declaration', 'required|string|max:500', function (Fluent $input) {
return 'RU' !== $input->destination_country && $input->weight > 2;
});
Таможенная декларация нужна для международных отправлений тяжелее 2 кг. Замыкание получает объект Fluent с исходными данными запроса и должно вернуть true, чтобы правило применилось. Обратите внимание: $input содержит сырые входные данные, а не результат валидации.
Можно применять правила сразу к нескольким полям:
$validator->sometimes(
['insurance_policy', 'insurance_amount'],
'required',
fn (Fluent $input) => 'freight' === $input->shipping_method
);
Условная валидация вложенных массивов
Второй аргумент замыкания – текущий элемент массива. Это решает задачу «разные правила для разных элементов»:
$validator = Validator::make($request->all(), [
'contacts' => 'required|array|min:1',
'contacts.*.type' => 'required|in:email,phone,url',
'contacts.*.value' => 'required|string',
]);
$validator->sometimes('contacts.*.value', 'email', function (Fluent $input, Fluent $item) {
return 'email' === $item->type;
});
$validator->sometimes('contacts.*.value', 'url', function (Fluent $input, Fluent $item) {
return 'url' === $item->type;
});
$validator->sometimes('contacts.*.value', 'regex:/^\+?\d{10,15}$/', function (Fluent $input, Fluent $item) {
return 'phone' === $item->type;
});
Правила валидации value зависят от type в том же элементе массива. Без $item пришлось бы вручную обходить индексы.
missing_if и missing_unless
Группа missing – самая строгая: поле не должно присутствовать в запросе вообще:
$request->validate([
'auth_method' => 'required|in:password,sso',
'password' => 'missing_if:auth_method,sso|required|string|min:8',
'sso_token' => 'missing_unless:auth_method,sso|required|string',
]);
При SSO-аутентификации поле password запрещено на уровне запроса. Не просто пустое – его не должно быть. Разница с prohibited: prohibited_if выбросит ошибку, если поле пришло с непустым значением, но пропустит null и "". missing_if отклонит поле, даже если оно пустое – ключ не должен присутствовать в запросе вообще. Для API, где клиент может отправить лишние поля по ошибке, missing надёжнее. О правилах валидации самого пароля (длина, символы, утечки) – в статье про валидацию паролей.
Связанные правила:
missing_with:foo,bar– поле не должно присутствовать, если хотя бы одно из перечисленных полей присутствует в запросеmissing_with_all:foo,bar– если все перечисленные поля присутствуют в запросе
Какое правило выбрать
У Laravel несколько семейств условных правил, и между ними легко запутаться. Вот логика выбора:
Поле обязательно при условии – required_if, required_unless, required_with, required_without. Поле остаётся в данных в любом случае, если оно заполнено.
Поле полностью исключить – exclude_if, exclude_unless, exclude_with, exclude_without. Поле убирается из validated(). Идеально для $model->fill().
Поле запрещено при условии – prohibited_if, prohibited_unless, prohibits. Если поле заполнено – ошибка валидации. Для защиты от некорректных данных с фронтенда.
Поле не должно быть в запросе – missing_if, missing_unless. Строже, чем prohibited: даже пустое значение вызовет ошибку.
Поле необязательно – sometimes (если не отправлено – пропустить), nullable (если null – допустить), filled (если отправлено – не пустое).
Практический ориентир: если данные идут в fill() модели – используйте exclude_*. Если нужно показать ошибку пользователю – prohibited_* или missing_*. Если просто условная обязательность – required_*.
Одно и то же поле с разными семействами правил ведёт себя по-разному:
// required_if: поле обязательно при type=premium, но если отправлено при type=free –
// всё равно попадёт в validated() и пройдёт валидацию
'feature_limit' => 'required_if:type,premium|integer|min:1',
// exclude_unless: поле полностью убирается из validated() при type != premium.
// Даже если клиент отправил значение – оно исчезнет
'feature_limit' => 'exclude_unless:type,premium|required|integer|min:1',
// prohibited_if: при type=free отправка поля вызовет ошибку валидации.
// Клиент получит сообщение об ошибке
'feature_limit' => 'prohibited_if:type,free|integer|min:1',
// missing_unless: при type != premium само присутствие ключа в запросе –
// ошибка. Даже feature_limit=null не пройдёт
'feature_limit' => 'missing_unless:type,premium|required|integer|min:1',
Выбор зависит от того, насколько строго нужно контролировать данные и что делать с лишними полями – молча отбросить или сообщить клиенту.
Комбинирование условий
Реальные формы редко ограничиваются одним условием. Вот полный пример формы оплаты:
use Illuminate\Validation\Rule;
class PaymentRequest extends FormRequest
{
protected function prepareForValidation(): void
{
$this->mergeIfMissing([
'save_card' => false,
]);
}
public function rules(): array
{
return [
'method' => 'required|in:card,bank_transfer,cash',
// Блок карты – исключается при других способах оплаты
'card_number' => 'exclude_unless:method,card|required|string|size:16',
'card_expiry' => 'exclude_unless:method,card|required|date_format:m/y',
'card_cvv' => 'exclude_unless:method,card|required|string|size:3',
'save_card' => 'exclude_unless:method,card|boolean',
// Банковский перевод
'bank_account' => 'exclude_unless:method,bank_transfer|required|string|max:34',
'bank_bic' => 'exclude_unless:method,bank_transfer|required|string|size:9',
// Наличные – ничего дополнительного
// Сумма и валюта – всегда
'amount' => 'required|numeric|min:0.01',
'currency' => 'required|in:RUB,USD,EUR',
// Комментарий обязателен при крупной сумме
'comment' => [
Rule::requiredIf(fn () => $this->float('amount') > 100_000),
'nullable',
'string',
'max:500',
],
];
}
}
Здесь exclude_unless для блоков, Rule::requiredIf() для зависимости от суммы, prepareForValidation() для значения по умолчанию – три механизма в одной форме, каждый под свою задачу.
Условная валидация во вложенных структурах
При работе с массивами объектов условные правила применяются через wildcard *:
$request->validate([
'items' => 'required|array|min:1',
'items.*.type' => 'required|in:product,service,subscription',
'items.*.quantity' => 'required|integer|min:1',
'items.*.recurring_months' => 'required_if:items.*.type,subscription|integer|min:1|max:36',
'items.*.license_key' => 'required_if:items.*.type,product|string',
'items.*.hourly_rate' => 'exclude_unless:items.*.type,service|required|numeric|min:0',
]);
required_if с wildcard сравнивает поле type того же элемента массива. Не нужно указывать индекс.
Для более сложной логики внутри элементов массива используйте Rule::forEach():
use Illuminate\Validation\Rule;
$request->validate([
'recipients' => 'required|array|min:1',
'recipients.*.channel' => 'required|in:email,sms,push',
'recipients.*' => Rule::forEach(function (mixed $value, string $attribute) {
$channel = data_get($value, 'channel');
$rules = ['channel' => 'required|in:email,sms,push'];
if ('email' === $channel) {
$rules['address'] = 'required|email';
} elseif ('sms' === $channel) {
$rules['address'] = 'required|regex:/^\+\d{10,15}$/';
} else {
$rules['address'] = 'required|string|max:255';
}
return $rules;
}),
]);
Rule::forEach() даёт полный контроль над правилами каждого элемента. Замыкание получает значение и имя атрибута, возвращает массив правил.
Типичные ошибки
required_if не отменяет остальные правила. Если условие required_if не выполнено, поле перестаёт быть обязательным – но другие правила в цепочке продолжают работать. Пустое поле пройдёт, а вот строка abc для правила integer – нет. Чтобы полностью убрать поле из валидации, нужен exclude_if или exclude_unless.
nullable не равно sometimes. nullable разрешает значение null. sometimes разрешает отсутствие поля. На PATCH-запросах обычно нужны оба.
Ставьте exclude_if первым в цепочке. Laravel обрабатывает exclude_* до остальных правил внутренне, но размещение в начале строки делает намерение очевидным при чтении кода. Это конвенция, а не техническое ограничение.
ConvertEmptyStringsToNull. Middleware превращает "" в null. Без nullable правило string отклонит поле с пустой строкой, потому что на вход придёт null. Если форма отправляет пустые строки для необязательных полей – добавьте nullable.
required_with при необязательных блоках. required_with:street сработает, даже если street содержит только пробелы (после trim middleware пустая строка станет null, и поле считается отсутствующим). Если блок адреса полностью необязателен, но внутри связан – required_with корректно обработает этот случай.
prohibits не делает поле обязательным. prohibits:other_field только запрещает other_field при заполнении текущего. Само текущее поле не становится обязательным.
Rule::forEach() возвращает массив. Rule::forEach() ожидает замыкание, которое возвращает массив правил. Документация показывает ассоциативный массив с вложенными полями – придерживайтесь этого формата.
Путаница между filled и required. filled не требует присутствия поля. Если поле не пришло – валидация пройдёт. required – обязательное присутствие. filled полезно, когда поле может отсутствовать, но если передано – оно не должно быть пустым.
Тестирование условной валидации
Условные правила требуют проверки каждой ветки. Минимум – два теста на каждое условие:
use Tests\TestCase;
class PaymentRequestTest extends TestCase
{
public function test_card_fields_required_for_card_payment(): void
{
$response = $this->postJson('/api/payments', [
'method' => 'card',
'amount' => 1500,
'currency' => 'RUB',
]);
$response->assertUnprocessable();
$response->assertJsonValidationErrors(['card_number', 'card_expiry', 'card_cvv']);
}
public function test_card_fields_excluded_for_bank_transfer(): void
{
$response = $this->postJson('/api/payments', [
'method' => 'bank_transfer',
'bank_account' => 'RU1234567890123456789012345678901234',
'bank_bic' => '044525225',
'amount' => 1500,
'currency' => 'RUB',
'card_number' => '4111111111111111',
]);
$response->assertOk();
// card_number не попадёт в validated(), даже если отправлен
}
public function test_comment_required_for_large_amounts(): void
{
$response = $this->postJson('/api/payments', [
'method' => 'cash',
'amount' => 200_000,
'currency' => 'RUB',
]);
$response->assertJsonValidationErrors(['comment']);
}
public function test_comment_optional_for_small_amounts(): void
{
$response = $this->postJson('/api/payments', [
'method' => 'cash',
'amount' => 500,
'currency' => 'RUB',
]);
$response->assertJsonMissingValidationErrors(['comment']);
}
public function test_pickup_and_delivery_mutually_exclusive(): void
{
$response = $this->postJson('/api/orders', [
'pickup_point_id' => 42,
'delivery_address' => 'ул. Ленина, 1',
]);
$response->assertJsonValidationErrors(['delivery_address']);
}
}
Обратите внимание на второй тест: мы намеренно отправляем card_number при банковском переводе, чтобы убедиться, что exclude_unless его отбросит.
Для проверки, что exclude действительно убирает поле из данных:
public function test_excluded_fields_not_in_validated_data(): void
{
$validator = Validator::make([
'method' => 'cash',
'amount' => 500,
'currency' => 'RUB',
'card_number' => '4111111111111111',
], [
'method' => 'required|in:card,cash',
'amount' => 'required|numeric',
'currency' => 'required|in:RUB,USD',
'card_number' => 'exclude_unless:method,card|required|string',
]);
$validated = $validator->validated();
$this->assertArrayNotHasKey('card_number', $validated);
}
Условные правила взаимодействуют друг с другом, и одна пропущенная ветка может открыть дыру: лишнее поле попадёт в базу, обязательное поле окажется пустым. Покрывайте каждое условие парой тестов – «условие выполнено» и «условие не выполнено». Если ветвлений больше двух (как в примере с оплатой) – тест на каждый вариант.
Подробнее о создании собственных правил – в статье про пользовательские правила валидации. О правилах min, max, between для чисел и строк – в руководстве по валидации строк и чисел. Работа с валидацией файлов и изображений – в руководстве по файловой валидации. Основы валидации и первые шаги – в руководстве по валидации.