Валидация в API
Отличия от web
Web: ошибки → редирект с flash-сессией. API: ошибки → JSON с кодом 422.
Ответ:
{
"message": "The title field is required. (and 2 more errors)",
"errors": {
"title": ["The title field is required."],
"body": ["The body field is required."],
"tags.0": ["The selected tags.0 is invalid."]
}
}
message — общее.
errors — по полям.
Form Request
Тот же подход, что и для web.
php artisan make:request StorePostRequest
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:3', 'max:255'],
'slug' => ['nullable', 'string', 'max:255', Rule::unique('posts', 'slug')],
'body' => ['required', 'string', 'min:10'],
'status' => ['required', Rule::in(['draft', 'published'])],
'published_at' => ['nullable', 'date', 'after:today'],
'tags' => ['array'],
'tags.*' => ['integer', 'exists:tags,id'],
];
}
public function messages(): array
{
return [
'title.required' => 'Заголовок обязателен',
'title.min' => 'Минимум 3 символа',
'body.required' => 'Текст обязателен',
];
}
public function attributes(): array
{
return [
'title' => 'заголовок',
'body' => 'текст поста',
];
}
}
Контроллер:
public function store(StorePostRequest $request)
{
$post = $request->user()->posts()->create(
$request->validated(),
);
return new PostResource($post);
}
Разные правила для store и update
Проблема: при update unique на slug не должен срабатывать на самого себя.
Решение: Rule::unique()->ignore().
class UpdatePostRequest extends FormRequest
{
public function rules(): array
{
$postId = $this->route('post')?->id;
return [
'title' => ['sometimes', 'string', 'min:3', 'max:255'],
'slug' => ['sometimes', 'string', Rule::unique('posts', 'slug')->ignore($postId)],
'body' => ['sometimes', 'string', 'min:10'],
];
}
}
sometimes — валидировать, только если поле есть в запросе. PATCH — частичное обновление.
Условная валидация
Rule::requiredIf:
'body' => Rule::requiredIf(fn () => $this->status === 'published'),
required_if:
'body' => 'required_if:status,published',
Rule::when:
'title' => Rule::when($isAdmin, 'required|max:255', 'required|max:100'),
Кастомные правила
php artisan make:rule Uppercase
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class Uppercase implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (strtoupper($value) !== $value) {
$fail('Поле :attribute должно быть в верхнем регистре');
}
}
}
Использование:
'title' => ['required', new Uppercase()],
Сообщения для клиента
По умолчанию — на английском.
Русские — в lang/ru/validation.php:
php artisan lang:publish
Или кастомные сообщения в Form Request.
Формат ошибок
Один стиль во всём API:
{
"message": "Ошибка валидации",
"errors": {
"title": ["Заголовок обязателен"]
}
}
Настроить глобально — bootstrap/app.php:
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (ValidationException $e, $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Ошибка валидации',
'errors' => $e->errors(),
], 422);
}
});
})
Проверка через PHPStan
Использование $request->validated() даёт типизированный массив.
$data = $request->validated();
// $data — array<string, mixed>
С кастомным Form Request:
/**
* @property string $title
* @property string $body
*/
class StorePostRequest extends FormRequest { }
Массивы
Простой:
'tags' => ['required', 'array', 'min:1'],
'tags.*' => ['string', 'max:50'],
Массив объектов:
'items' => ['required', 'array'],
'items.*.product_id' => ['required', 'integer', 'exists:products,id'],
'items.*.qty' => ['required', 'integer', 'min:1', 'max:100'],
Валидация с transform
Иногда данные нужно преобразовать:
protected function prepareForValidation(): void
{
$this->merge([
'slug' => $this->slug ?: Str::slug($this->title),
'email' => strtolower($this->email),
]);
}
Пример: post
// app/Http/Requests/StorePostRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Support\Str;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:3', 'max:255'],
'slug' => ['nullable', 'string', 'max:255', Rule::unique('posts', 'slug')],
'body' => ['required', 'string', 'min:10', 'max:50000'],
'status' => ['required', Rule::in(['draft', 'published'])],
'published_at' => ['nullable', 'date', 'after:today'],
'tags' => ['nullable', 'array', 'max:10'],
'tags.*' => ['integer', 'exists:tags,id'],
];
}
public function messages(): array
{
return [
'title.required' => 'Заголовок обязателен',
'title.min' => 'Минимум 3 символа',
'title.max' => 'Максимум 255 символов',
'body.min' => 'Текст минимум 10 символов',
'status.in' => 'Статус может быть draft или published',
'published_at.after' => 'Дата должна быть в будущем',
'tags.max' => 'Максимум 10 тегов',
'tags.*.exists' => 'Такого тега не существует',
];
}
public function attributes(): array
{
return [
'title' => 'заголовок',
'body' => 'текст',
'published_at' => 'дата публикации',
];
}
protected function prepareForValidation(): void
{
$this->merge([
'slug' => $this->slug ?: Str::slug($this->title),
]);
}
}
Практика
Создай Form Request:
php artisan make:request StorePostRequest
Опиши правила, сообщения, атрибуты.
Используй в контроллере:
public function store(StorePostRequest $request)
{
return new PostResource(
Post::create($request->validated())
);
}
Проверь:
curl -X POST http://localhost:8000/api/posts \
-H "Content-Type: application/json" \
-d '{"title": "a"}'
# 422 с ошибками
Частые ошибки
1. $request->all() вместо validated():
Post::create($request->all()); // лишние поля
// Правильно:
Post::create($request->validated());
2. authorize возвращает false по умолчанию:
public function authorize(): bool { }
// → 403
// Правильно:
return true; // или проверка прав
3. unique без ignore при update:
Rule::unique('posts', 'slug') // не даст обновить
// Правильно:
Rule::unique('posts', 'slug')->ignore($postId)
4. sometimes забыли:
// PATCH с одним полем — остальные обязательны
'title' => 'required' // ошибка
// Правильно:
'title' => 'sometimes|required'
5. Массивы без .*:
'tags' => 'integer' // сработает на массив
// Правильно:
'tags' => 'array',
'tags.*' => 'integer'
Итог
- Form Request — правила в классе.
- 422 — код ошибки валидации.
messages,attributes— кастомизация.prepareForValidation—预处理.Rule::unique()->ignore— для update.sometimes— для PATCH.- Единый формат ошибок — важно для клиента.
Дальше — пагинация в API.