Сообщения об ошибках валидации Laravel 13
При провале валидации Laravel генерирует ошибки и доставляет их клиенту – через редирект с flash-данными для веб-форм или JSON-ответ для API. Механизм работает из коробки, но по умолчанию сообщения английские и привязаны к техническим именам полей. В реальном проекте этого мало: нужны собственные тексты ошибок, перевод на язык пользователя, корректное отображение в Blade и обработка JSON-ответов на фронтенде.
Основы работы с валидацией описаны в отдельной статье. Здесь разбираем всё, что касается ошибок: от внутреннего устройства до полной локализации.
Как Laravel обрабатывает ошибки
Любой из способов валидации – $request->validate(), Form Request, Validator::make()->validate() – при провале выбрасывает Illuminate\Validation\ValidationException. Дальнейшее зависит от типа запроса:
- Обычный HTTP-запрос - Laravel перехватывает исключение в exception handler, сохраняет ошибки и старый ввод в сессию, возвращает редирект на предыдущую страницу с кодом 302.
- XHR / JSON-запрос (заголовок
Accept: application/json) - возвращает JSON с ошибками и HTTP-статус 422 Unprocessable Entity.
Это поведение определяется в Illuminate\Foundation\Exceptions\Handler. Перехватывать исключение вручную не нужно, если только вы не хотите изменить стандартную логику.
Важная деталь: Laravel определяет, ожидает ли клиент JSON, через метод $request->expectsJson(). Он проверяет заголовок Accept. Если фронтенд отправляет форму через обычный POST без AJAX, ответом будет редирект, а не JSON. Когда в тестах вместо postJson() используется post(), ответ тоже будет редиректом – это частая причина путаницы при написании тестов для API.
HTTP-статус и формат ответа
Веб-запрос
При неудачной валидации обычного POST-запроса Laravel:
- Складывает ошибки в
session()->flash('errors', ...) - Складывает ввод в
session()->flash('_old_input', ...) - Возвращает
302 Foundнаurl()->previous()
Код 302 – не ошибка валидации как таковая, а стандартный HTTP-редирект. Собственно ошибки приходят через сессию. Поэтому при работе с API-роутами без сессий (группа api) механизм flash-данных недоступен и нужен JSON-формат.
JSON-ответ
Для запросов, ожидающих JSON, Laravel формирует ответ с кодом 422:
{
"message": "The name field is required. (and 2 more errors)",
"errors": {
"name": [
"The name field is required."
],
"email": [
"The email field must be a valid email address.",
"The email field must not be greater than 255 characters."
]
}
}
Ключ message содержит первую ошибку и число оставшихся. Ключ errors – объект, где каждое поле сопоставлено с массивом строк. Вложенные поля используют dot-нотацию: address.city, items.0.name.
На фронтенде этот формат обрабатывается так:
try {
await axios.post('/api/orders', data);
} catch (error) {
if (422 === error.response?.status) {
const errors = error.response.data.errors;
Object.entries(errors).forEach(([field, messages]) => {
showFieldError(field, messages[0]);
});
}
}
Код 422 – стандарт для ошибок валидации в REST API. Некоторые проекты предпочитают 400 Bad Request, но Laravel отдаёт именно 422. Изменить статус можно через ValidationException – об этом ниже.
Если API-клиентам нужен машиночитаемый код ошибки помимо текста, добавьте его в кастомный формат ответа. Стандартный JSON Laravel не содержит отдельного поля code – только message и errors. Добавить своё поле можно через переопределение failedValidation() в Form Request или глобально в bootstrap/app.php (раздел про модификацию JSON-ответа ниже).
Переменная $errors в Blade
Middleware ShareErrorsFromSession из группы web автоматически передаёт переменную $errors во все представления. Это экземпляр Illuminate\Support\MessageBag. Переменная доступна всегда, даже когда ошибок нет – в этом случае $errors->isEmpty() вернёт true.
Передавать $errors вручную через view('form', ['errors' => ...]) не нужно – middleware делает это за вас. Если вы работаете вне группы web (например, в API-контроллере, который рендерит HTML), $errors может быть не определена – middleware ShareErrorsFromSession не подключен.
Вывод ошибок в шаблоне
Общий блок ошибок над формой:
@if ($errors->any())
<div class="bg-red-50 border border-red-200 rounded p-4 mb-6">
<ul>
@foreach ($errors->all() as $error)
<li class="text-red-700 text-sm">{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
Сообщение рядом с конкретным полем:
<input type="email" name="email" value="{{ old('email') }}"
class="@error('email') border-red-500 @enderror">
@error('email')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
Директива @error принимает имя поля и предоставляет переменную $message с текстом первой ошибки. Для подсветки поля с ошибкой удобно использовать @error прямо в атрибуте class, как в примере выше – добавить CSS-класс только при наличии ошибки.
Для вложенных полей применяется dot-нотация:
@error('address.city')
<p class="text-sm text-red-600">{{ $message }}</p>
@enderror
Компонент Blade, который оборачивает вывод ошибки для любого поля:
{{-- resources/views/components/field-error.blade.php --}}
@props(['field'])
@error($field)
<p {{ $attributes->merge(['class' => 'text-red-600 text-sm mt-1']) }}>
{{ $message }}
</p>
@enderror
Использование:
<x-field-error field="email" />
Методы MessageBag
Класс MessageBag предоставляет набор методов для работы с коллекцией ошибок.
Первая ошибка поля
$errors->first('email');
// "The email field is required."
Без аргумента first() вернёт первую ошибку первого поля. Полезно для вывода единственного сообщения, например в мобильном приложении.
Все ошибки конкретного поля
$errors->get('email');
// ["The email field is required.", "The email must be valid."]
Возвращает массив строк. Если у поля ошибок нет, вернётся пустой массив.
Для элементов массива с подстановочным знаком:
$errors->get('items.*');
// ['items.0' => [...], 'items.1' => [...]]
Все ошибки всех полей
$errors->all();
// ["The name is required.", "The email is required.", ...]
Плоский массив сообщений в порядке объявления полей. Подходит для вывода общего блока ошибок.
Проверка наличия
if ($errors->has('email')) {
// у поля email есть хотя бы одна ошибка
}
if ($errors->any()) {
// есть хотя бы одна ошибка по любому полю
}
if ($errors->isEmpty()) {
// ошибок нет
}
$errors->count(); // общее количество сообщений
Сериализация
$errors->toArray();
// ['email' => ['...'], 'name' => ['...']]
$errors->toJson();
// {"email":["..."],"name":["..."]}
Метод toArray() возвращает ассоциативный массив поле => [сообщения]. Если нужен плоский массив, используйте all().
Именованные error bags
Когда на одной странице несколько форм со своей валидацией, ошибки одной формы могут смешаться с ошибками другой. Именованные error bags разделяют их. Типичный пример – страница профиля с формой изменения email и формой смены пароля.
В контроллере:
$request->validateWithBag('login', [
'email' => 'required|email',
'password' => 'required',
]);
При ручном создании валидатора:
return redirect('/settings')
->withErrors($validator, 'profile');
В Blade – доступ через имя пакета:
<h3>Форма логина</h3>
@if ($errors->login->any())
<div class="alert">
{{ $errors->login->first('email') }}
</div>
@endif
<h3>Форма профиля</h3>
@if ($errors->profile->any())
<div class="alert">
{{ $errors->profile->first('name') }}
</div>
@endif
Директива @error тоже принимает имя bag вторым аргументом:
@error('email', 'login')
<p>{{ $message }}</p>
@enderror
Пакет по умолчанию называется default. Обращение к $errors->first('email') эквивалентно $errors->default->first('email').
Кастомные сообщения об ошибках
Англоязычные сообщения из коробки – не то, что видит пользователь в продакшне. Переопределить их можно на нескольких уровнях.
Через validate()
Второй аргумент validate() принимает массив сообщений:
$request->validate([
'title' => 'required|max:200',
'body' => 'required',
], [
'title.required' => 'Укажите заголовок статьи.',
'title.max' => 'Заголовок не может быть длиннее :max символов.',
'body.required' => 'Текст статьи обязателен.',
]);
Ключи формируются по принципу поле.правило. Можно задать сообщение только для правила – тогда оно применится ко всем полям:
$request->validate($rules, [
'required' => 'Поле :attribute обязательно.',
'email.required' => 'Укажите email.',
]);
Сообщение email.required имеет приоритет над общим required для поля email.
Через Validator::make
Третий аргумент – сообщения, четвёртый – имена полей:
$validator = Validator::make($data, $rules, [
'required' => 'Поле :attribute обязательно для заполнения.',
'email' => 'Поле :attribute должно быть корректным адресом.',
], [
'category_id' => 'категория',
]);
В Form Request
class StoreArticleRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => 'required|max:200',
'category_id' => 'required|exists:categories,id',
];
}
public function messages(): array
{
return [
'title.required' => 'Без заголовка статью не сохранить.',
'category_id.required' => 'Выберите категорию.',
'category_id.exists' => 'Такой категории не существует.',
];
}
}
Подробнее о Form Request – в соответствующей статье.
Переопределение имён полей
Сообщение The category_id field is required нечитаемо для пользователя. Вместо переписывания каждого текста ошибки проще заменить имя поля.
Четвёртый аргумент Validator::make
$validator = Validator::make($data, $rules, $messages, [
'category_id' => 'категория',
'first_name' => 'имя',
'last_name' => 'фамилия',
]);
Теперь сообщение станет: Поле категория обязательно для заполнения.
Метод attributes() в Form Request
public function attributes(): array
{
return [
'category_id' => 'категория',
'email' => 'электронная почта',
'phone' => 'номер телефона',
];
}
Работает в паре с messages(). Если сообщение содержит плейсхолдер :attribute, Laravel подставит значение из этого массива. Если messages() содержит полный текст без :attribute, переопределение имени не повлияет на этот текст.
Приоритет сообщений
Laravel ищет текст ошибки в нескольких местах и берёт первое найденное. Порядок поиска:
- Инлайн-сообщения (аргумент
validate(),messages()в Form Request, третий аргументValidator::make) - Секция
customвlang/{locale}/validation.php– комбинацияполе.правило - Основной массив в
lang/{locale}/validation.php– по имени правила - JSON-переводы в
lang/{locale}.json
Пример: для поля email с правилом required Laravel сначала проверит $messages['email.required'], затем custom.email.required в языковом файле, затем общее required в языковом файле.
Это позволяет комбинировать уровни: глобальный перевод через языковые файлы для типичных сообщений и точечное переопределение через messages() для конкретных бизнес-контекстов.
Плейсхолдеры в сообщениях
Текст ошибки может содержать плейсхолдеры, которые Laravel автоматически заменяет реальными значениями:
| Плейсхолдер | Что подставляется |
|---|---|
:attribute | Имя поля (или значение из attributes()) |
:input | Текущее значение проверяемого поля |
:other | Имя другого поля (для same, different, gt, lt) |
:value | Значение параметра правила (для required_if, declined_if, accepted_if - сравниваемое значение, не значение поля) |
:min, :max, :size | Параметры правила |
:values | Список допустимых значений (для in, not_in) |
:date | Параметр-дата (для правил after, before) |
:index | Числовой индекс элемента массива (с нуля) |
:position | Позиция элемента (с единицы) |
Пример с несколькими плейсхолдерами:
$messages = [
'price.between' => 'Цена :input выходит за диапазон от :min до :max рублей.',
'end_date.after' => 'Дата окончания должна быть позже :date.',
'password.same' => 'Пароль и :other должны совпадать.',
];
Некоторые правила имеют разные сообщения для разных типов данных. Правило max для строк проверяет длину в символах, для чисел – значение, для файлов – размер в килобайтах. В языковом файле это выражается вложенным массивом:
'max' => [
'string' => ':attribute не длиннее :max символов.',
'numeric' => ':attribute не больше :max.',
'file' => ':attribute не больше :max КБ.',
'array' => ':attribute не более :max элементов.',
],
Laravel сам определяет тип поля по другим правилам в цепочке (наличие numeric, integer, file, array) и выбирает подходящий вариант.
Плейсхолдеры :index и :position работают в ошибках массивов. Для порядковых числительных на английском есть :ordinal-position (1st, 2nd, 3rd), но в русской локали его лучше заменить на :position с ручным текстом.
Свои плейсхолдеры
Через метод Validator::replacer() можно зарегистрировать собственный плейсхолдер для пользовательского правила. Обычно это делается в AppServiceProvider:
use Illuminate\Support\Facades\Validator;
public function boot(): void
{
Validator::replacer('max_words', function ($message, $attribute, $rule, $params) {
return str_replace(':max_words', $params[0], $message);
});
}
После этого в сообщении для правила max_words можно использовать :max_words:
'bio.max_words' => 'Биография не должна превышать :max_words слов.',
Для пользовательских Rule-классов плейсхолдеры задаются прямо в методе message() – отдельный replacer не нужен.
Сообщения в пользовательских правилах
Когда встроенных правил недостаточно и вы создаёте собственный Rule-класс, текст ошибки задаётся в методе message():
class PhoneFormat implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (0 === preg_match('/^\+7\d{10}$/', $value)) {
$fail('Поле :attribute должно содержать номер в формате +7XXXXXXXXXX.');
}
}
}
Вызов $fail() с ключом перевода позволяет локализовать сообщение:
$fail('validation.phone_format')->translate();
При этом в lang/ru/validation.php добавляется:
'phone_format' => 'Номер :attribute должен быть в формате +7XXXXXXXXXX.',
Замыкания (closures) работают аналогично – $fail() принимает строку или ключ перевода:
$request->validate([
'username' => [
'required',
'string',
function ($attribute, $value, $fail) {
if (str_contains($value, ' ')) {
$fail('Имя пользователя не должно содержать пробелов.');
}
},
],
]);
Языковые файлы валидации
Для полной локализации ошибок используются языковые файлы в директории lang/.
Публикация
По умолчанию директории lang в свежем проекте нет. Создаётся командой:
php artisan lang:publish
Появляется lang/en/validation.php со всеми стандартными сообщениями. Файл содержит переводы для каждого встроенного правила.
Структура validation.php
Файл lang/ru/validation.php содержит основной массив сообщений и три дополнительных секции:
return [
'required' => 'Поле :attribute обязательно.',
'email' => 'Поле :attribute должно быть email-адресом.',
'max' => [
'string' => 'Поле :attribute не должно превышать :max символов.',
'numeric' => 'Поле :attribute не должно быть больше :max.',
'file' => 'Файл :attribute не должен превышать :max КБ.',
'array' => 'Поле :attribute не должно содержать более :max элементов.',
],
// ...
'custom' => [
'promo_code' => [
'exists' => 'Такого промокода не существует.',
],
],
'attributes' => [
'email' => 'электронная почта',
'password' => 'пароль',
'first_name' => 'имя',
'phone' => 'телефон',
'category_id' => 'категория',
],
'values' => [
'payment_type' => [
'cc' => 'банковская карта',
'wire' => 'банковский перевод',
],
],
];
Секция custom
Задаёт сообщения для конкретных комбинаций поле + правило централизованно. Не нужно повторять одно и то же в каждом Form Request:
'custom' => [
'subscription_end' => [
'after' => 'Дата окончания подписки должна быть в будущем.',
],
'avatar' => [
'dimensions' => 'Аватар должен быть не менее 200x200 пикселей.',
],
],
Удобно для сквозных полей, которые встречаются в нескольких формах – email, phone, password. Вместо дублирования messages() в десяти Form Request, один раз пишете в custom и забываете.
Секция attributes
Подмена технических имён полей на человекочитаемые. Действует глобально – затрагивает все сообщения, где встречается :attribute:
'attributes' => [
'body' => 'текст',
'expired_at' => 'дата истечения',
'qty' => 'количество',
],
Вместо Поле expired_at обязательно пользователь увидит Поле дата истечения обязательно.
Для полей массивов с подстановочным знаком:
'attributes' => [
'items.*.name' => 'название товара',
'items.*.price' => 'цена товара',
],
Секция values
Подмена значений полей в сообщениях. Полезна, когда сырое значение из формы нечитаемо:
'values' => [
'status' => [
'pending' => 'ожидает обработки',
'approved' => 'одобрен',
'rejected' => 'отклонён',
],
'role' => [
'admin' => 'администратор',
'moderator' => 'модератор',
],
],
Сообщение правила required_if:status,pending вместо ...when status is pending выведет текст с подставленным значением ожидает обработки. Подробнее об условных правилах – в отдельной статье.
Локализация: добавление языков
Готовые пакеты переводов
Переводить каждое из ~150 встроенных сообщений вручную необязательно. Пакет laravel-lang/common содержит переводы для десятков языков:
composer require laravel-lang/common --dev
php artisan lang:add ru
Команда создаст lang/ru/validation.php с переведёнными сообщениями. Пакет также добавляет переводы для пагинации, аутентификации и паролей.
После установки просмотрите сгенерированные переводы. Машинные переводы не всегда звучат естественно, и отдельные формулировки может потребоваться подправить вручную – секции custom и attributes пакет не заполняет, их вы пишете сами.
Ручное создание
mkdir -p lang/ru
cp lang/en/validation.php lang/ru/validation.php
Затем переведите содержимое файла. Трудоёмко, но даёт полный контроль над формулировками.
Переключение локали
Глобальная локаль задаётся в config/app.php:
'locale' => 'ru',
'fallback_locale' => 'en',
Для динамического переключения в зависимости от URL-сегмента или настроек пользователя:
// Middleware определяет локаль из URL
public function handle(Request $request, Closure $next)
{
$locale = $request->segment(1);
if (in_array($locale, config('app.supported_locales', ['ru', 'en']))) {
App::setLocale($locale);
}
return $next($request);
}
// Или по предпочтению пользователя
App::setLocale($user->language);
Fallback-локаль используется, когда перевод для текущей локали не найден. Обычно это en – если lang/ru/validation.php не содержит ключа, Laravel обратится к lang/en/validation.php.
JSON-переводы
Помимо PHP-файлов Laravel поддерживает переводы в lang/ru.json. Для валидации этот подход используется реже, но пригодится для точечного переопределения:
{
"The :attribute field is required.": "Поле :attribute обязательно для заполнения.",
"The :attribute field must be a valid email address.": ":attribute должен быть корректным email."
}
PHP-файлы имеют приоритет. Если ключ найден в lang/ru/validation.php, JSON-версия не применяется. JSON-переводы больше подходят для текстов интерфейса (кнопки, надписи), чем для валидации.
Ручное добавление ошибок
Иногда ошибка не связана с конкретным правилом валидации – внешний сервис вернул отказ, бизнес-проверка не пройдена, промокод просрочен. Добавить ошибку можно через хук after():
$validator = Validator::make($data, [
'card_token' => 'required|string',
'amount' => 'required|numeric|min:1',
]);
$validator->after(function ($v) use ($gateway) {
$charge = $gateway->preAuthorize($v->getData()['card_token']);
if (false === $charge->success) {
$v->errors()->add('payment', 'Платёж отклонён: ' . $charge->decline_reason);
}
});
if ($validator->fails()) {
return back()->withErrors($validator);
}
Хук after() вызывается после стандартной проверки правил. Если правила уже выявили ошибки, хук всё равно выполнится – вы получите и ошибки правил, и ваши бизнес-ошибки. Для контроля над этим проверяйте $v->errors()->isEmpty() внутри хука:
$validator->after(function ($v) use ($gateway) {
if ($v->errors()->isNotEmpty()) {
return; // пропустить внешний запрос, если уже есть ошибки
}
// ...проверка через внешний сервис
});
Для объединения ошибок из нескольких источников – метод merge():
$externalErrors = new MessageBag([
'shipping' => ['Доставка в указанный регион недоступна.'],
]);
$validator->errors()->merge($externalErrors);
Перехват ValidationException
В большинстве случаев Laravel обрабатывает ValidationException автоматически. Ручной перехват нужен при работе с очередями, консольными командами или если требуется нестандартная обработка.
use Illuminate\Validation\ValidationException;
try {
$validated = $request->validate([
'amount' => 'required|numeric|min:1',
'email' => 'required|email',
]);
} catch (ValidationException $e) {
$errors = $e->errors(); // ['amount' => [...], 'email' => [...]]
$status = $e->status; // 422
$bag = $e->errorBag; // 'default'
logger()->warning('Validation failed', [
'errors' => $errors,
'input' => $request->except('password'),
]);
return response()->json([
'success' => false,
'errors' => $errors,
], $status);
}
Ключевые свойства и методы исключения:
$e->errors() // массив ошибок [поле => [сообщения]]
$e->status // HTTP-статус, по умолчанию 422
$e->errorBag // имя error bag
$e->validator // экземпляр валидатора
$e->response // готовый Response, если задан
$e->redirectTo // URL для редиректа
Генерация с произвольными ошибками
throw ValidationException::withMessages([
'coupon' => ['Промокод уже использован.'],
'total' => ['Минимальная сумма заказа - 500 рублей.'],
]);
Задействует стандартный механизм обработки ошибок без создания отдельного класса исключения. Удобно в сервисных классах, где нет доступа к $request->validate(), но нужно вернуть ошибки в том же формате.
Изменение статуса
throw ValidationException::withMessages([
'account' => ['Аккаунт заблокирован.'],
])->status(403);
Статус 403 вместо 422 – для случаев, когда провал валидации связан не с форматом данных, а с правами доступа.
Указание URL редиректа
throw ValidationException::withMessages([
'step' => ['Заполните обязательные поля.'],
])->redirectTo('/checkout/step-1');
Модификация JSON-ответа
Стандартный формат JSON-ответа подходит для большинства SPA и мобильных клиентов. Если API требует другую структуру, переопределите обработку в Form Request:
// app/Http/Requests/ApiFormRequest.php
protected function failedValidation(Validator $validator): void
{
throw new HttpResponseException(
response()->json([
'status' => 'error',
'code' => 'VALIDATION_FAILED',
'errors' => $validator->errors()->toArray(),
], 422)
);
}
Глобальное переопределение – в bootstrap/app.php:
use Illuminate\Validation\ValidationException;
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (ValidationException $e) {
return response()->json([
'ok' => false,
'errors' => $e->errors(),
], $e->status);
});
})
Второй подход меняет формат для всего приложения. Первый – только для запросов, которые используют конкретный Form Request. В проектах с гибридным подходом (Blade + API) обычно делают базовый ApiFormRequest, от которого наследуются все API-запросы.
Ошибки в массивах
При валидации массивов ошибки привязываются к конкретному элементу через dot-нотацию:
$request->validate([
'items' => 'required|array|min:1',
'items.*.name' => 'required|string',
'items.*.qty' => 'required|integer|min:1',
]);
Если третий элемент не прошёл проверку, ключ ошибки – items.2.name. В шаблоне:
@foreach ($items as $i => $item)
<input type="text" name="items[{{ $i }}][name]"
value="{{ old("items.{$i}.name") }}">
@error("items.{$i}.name")
<span class="text-red-600">{{ $message }}</span>
@enderror
@endforeach
В кастомных сообщениях доступны плейсхолдеры позиции:
$messages = [
'items.*.name.required' => 'Укажите название товара #:position.',
'items.*.qty.min' => 'Количество товара #:position не менее :min.',
];
Сохранение введённых данных
Laravel автоматически сохраняет старый ввод в сессию при провале валидации. Функция old() в Blade возвращает предыдущее значение поля:
<input type="text" name="title" value="{{ old('title') }}">
<textarea name="body">{{ old('body') }}</textarea>
<select name="category">
@foreach ($categories as $cat)
<option value="{{ $cat->id }}"
@selected(old('category') == $cat->id)>
{{ $cat->name }}
</option>
@endforeach
</select>
Для файлов этот механизм не работает – содержимое загруженного файла не хранится в сессии. Если форма содержит и файлы, и текстовые поля, текстовые значения восстановятся, а файл придётся загрузить заново.
У old() есть второй аргумент – значение по умолчанию. При редактировании записи это позволяет совместить модель и старый ввод:
<input type="text" name="title"
value="{{ old('title', $article->title) }}">
Если валидация провалилась, подставится старый ввод. Если страница открыта впервые, подставится значение из модели.
Редирект на конкретную страницу
По умолчанию Laravel возвращает пользователя на предыдущий URL. Для многошаговых форм или нестандартных сценариев это можно изменить.
В Form Request:
class CheckoutRequest extends FormRequest
{
protected $redirect = '/checkout/payment';
// Или именованный роут:
// protected $redirectRoute = 'checkout.payment';
}
Частые проблемы
Ошибки не отображаются в Blade. Проверьте, что маршрут входит в группу middleware web. Без ShareErrorsFromSession переменная $errors не создаётся.
API-запрос возвращает 302 вместо 422. Убедитесь, что запрос отправляется с заголовком Accept: application/json. Без него Laravel считает запрос обычным HTTP и делает редирект. В тестах используйте postJson() вместо post().
Сообщение на английском, хотя перевод есть. Проверьте, что локаль установлена до вызова валидации. Если middleware SetLocale стоит после валидации в цепочке middleware, перевод не подтянется.
Ключ перевода вместо текста. Сообщение вида validation.required означает, что файл lang/{locale}/validation.php не найден или не содержит нужный ключ. Убедитесь, что файл существует и команда lang:publish выполнена.
Тестирование ошибок валидации
Laravel предоставляет assertion-методы для проверки ошибок в тестах.
Веб-запросы
public function test_title_required(): void
{
$response = $this->post('/articles', [
'body' => 'Текст статьи',
]);
$response->assertSessionHasErrors(['title']);
// Проверка текста сообщения
$response->assertSessionHasErrors([
'title' => 'Укажите заголовок статьи.',
]);
}
JSON API
public function test_api_returns_422(): void
{
$response = $this->postJson('/api/articles', []);
$response->assertStatus(422)
->assertJsonValidationErrors(['title', 'body'])
->assertJson([
'errors' => [
'title' => ['Укажите заголовок статьи.'],
],
]);
}
Отсутствие ошибок
$response = $this->post('/articles', [
'title' => 'Новая статья',
'body' => 'Содержание.',
]);
$response->assertSessionHasNoErrors();
Именованные bags в тестах
$response->assertSessionHasErrors(
keys: ['email'],
errorBag: 'login'
);
Проверка конкретного поля без ошибки
$response->assertSessionDoesntHaveErrors(['title']);
Проверка ValidationException
public function test_service_validates_input(): void
{
$this->expectException(ValidationException::class);
$service = new OrderService;
$service->process(['amount' => -1]);
}
Если нужно проверить и текст ошибки:
public function test_service_error_message(): void
{
try {
$service = new OrderService;
$service->process(['amount' => -1]);
$this->fail('Expected ValidationException');
} catch (ValidationException $e) {
$this->assertArrayHasKey('amount', $e->errors());
$this->assertStringContainsString('больше нуля', $e->errors()['amount'][0]);
}
}
О тестировании пользовательских правил и проверке полей по базе данных – в отдельных разделах. Валидация строк и чисел разобрана со своими примерами.