Форма и CSRF:
За защиту от CSRF для маршрутов из группы web (всё, что лежит в routes/web.php) отвечает middleware Illuminate\Foundation\Http\Middleware\PreventRequestForgery. Если в статьях встретите имя ValidateCsrfToken, это он же: так класс назывался в Laravel 11 и 12, а в 13-й версии старое имя оставлено пустым классом-алиасом с пометкой @deprecated. Проверка идёт в два шага. Сначала middleware смотрит заголовок Sec-Fetch-Site, который современные браузеры выставляют сами: запрос со значением same-origin проходит без токена вообще. Если заголовка нет (curl, старые браузеры, часть HTTP-клиентов) или запрос пришёл с чужого сайта, нужен валидный CSRF-токен, иначе Laravel ответит ошибкой 419. На API-маршруты проверка не распространяется, а отдельные URI можно вывести из-под неё через preventRequestForgery(except: [...]) в bootstrap/app.php. Старый метод validateCsrfTokens(except: [...]) из 11/12 ещё работает, но в 13-й помечен @deprecated и просто оборачивает preventRequestForgery, в новом коде используйте актуальное имя. Токен добавляет директива @csrf, она рендерит скрытое поле _token. И да, @csrf по-прежнему ставим в каждую форму: токен нужен, чтобы легитимные клиенты без заголовка Sec-Fetch-Site не ловили 419. Создаём resources/views/posts/create.blade.php:
Код: Выделить всё
<form method="POST" action="{{ route('posts.store') }}">
@csrf
<input type="text" name="title" value="{{ old('title') }}" placeholder="Заголовок">
@error('title') <p class="error">{{ $message }}</p> @enderror
<textarea name="body" placeholder="Текст поста">{{ old('body') }}</textarea>
@error('body') <p class="error">{{ $message }}</p> @enderror
<input type="date" name="published_at" value="{{ old('published_at') }}">
@error('published_at') <p class="error">{{ $message }}</p> @enderror
<button type="submit">Опубликовать</button>
</form>
Валидация в контроллере:
Маршруты posts.create и posts.store вы напишете сами, это материал второй главы. Метод store в PostController:
Код: Выделить всё
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string', 'min:50'],
'published_at' => ['nullable', 'date'],
]);
Post::create($validated);
return redirect()->route('posts.index')->with('status', 'Пост сохранён');
}
Form Request:
Когда правил десяток и больше, контроллер распухает. Выносим проверку в класс командой
Код: Выделить всё
php artisan make:request StorePostRequestКод: Выделить всё
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string', 'min:50'],
'published_at' => ['nullable', 'date'],
];
}
public function messages(): array
{
return [
'body.min' => 'Текст слишком короткий, нужно хотя бы :min символов.',
];
}
}
Сообщения по умолчанию на английском. Поставьте пакет переводов:
Код: Выделить всё
composer require laravel-lang/common --devКод: Выделить всё
php artisan lang:add ruРучная валидация и свои правила:
Иногда validate не подходит: при ошибке нужно не редиректить, а сделать что-то своё, залогировать, ответить нестандартно. Тогда собираем валидатор руками:
Код: Выделить всё
use Illuminate\Support\Facades\Validator;
$validator = Validator::make($request->all(), [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string', 'min:50'],
]);
if ($validator->fails()) {
// свой сценарий: лог, кастомный ответ, другой редирект
}
$validated = $validator->validated();
Код: Выделить всё
'title' => ['required', 'string', 'max:255', function ($attribute, $value, $fail) {
if (str_contains(mb_strtolower($value), 'спам')) {
$fail('Заголовок содержит запрещённое слово.');
}
}],
Код: Выделить всё
php artisan make:rule NoSpamWordsЕщё два инструмента управления проверкой. Правило bail останавливает проверку конкретного поля на первой ошибке: в ['bail', 'required', 'string', 'max:255'] при провале required правила string и max уже не запустятся. А метод stopOnFirstFailure() на валидаторе (или свойство $stopOnFirstFailure = true в Form Request) обрывает всю проверку целиком после первой ошибки любого поля.
В шаблоне, кроме @error по одному полю, можно вывести все ошибки списком:
Код: Выделить всё
@if ($errors->any())
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif
Ошибка 419 Page Expired при отправке. В Laravel 13 из обычного браузера на своём же домене её почти не увидеть: same-origin запрос проходит по заголовку Sec-Fetch-Site даже с забытым @csrf. Ловится 419 там, где заголовка нет или запрос cross-site: curl и HTTP-клиенты, старые браузеры, отправка с чужого домена. Для таких клиентов причины классические: забытый @csrf или страница, провисевшая открытой дольше жизни сессии (токен протух). На Laravel 11/12 заголовок не проверяется, и там 419 это по-прежнему почти всегда забытый @csrf.
Пустое необязательное поле роняет валидацию. Браузер шлёт пустую строку, middleware ConvertEmptyStringsToNull превращает её в null, и правило date на null падает. Лекарство: добавить nullable первым правилом, как у published_at выше.
Невыбранный чекбокс браузер не отправляет вовсе, а выбранный без явного value уходит со значением "on". Правило required для него бессмысленно, а связка ['sometimes', 'boolean'] на "on" провалится: boolean принимает только true, false, 1, 0, "1" и "0". Рабочих варианта три. Первый: задать в разметке value="1", тогда ['sometimes', 'boolean'] отработает. Второй: для чекбокса, который обязан быть отмечен (согласие с правилами), взять правило accepted, оно понимает и "on", и "yes". Третий: нормализовать значение в Form Request до запуска проверки:
Код: Выделить всё
protected function prepareForValidation(): void
{
$this->merge(['is_draft' => $this->boolean('is_draft')]);
}
MassAssignmentException при сохранении. Значит, в модели Post не заполнен $fillable, вернитесь к пятой главе.
Что усвоили:
Форма с @csrf и old(), validate в контроллере, Form Request для крупных форм, @error и $errors в шаблоне, validated() вместо all(), а для нестандартных случаев Validator::make и собственные правила. На этом фундаменте стоит вся восьмая глава: логин и регистрация, это те же формы с той же валидацией.