Проблема
Після відправки POST-форми, якщо повернути view напряму, виникають дві проблеми. Перша — натискання F5 або кнопки “Назад” покаже попередження браузера “Підтвердити повторну відправку форми?“. Друга — якщо користувач підтвердить, дані відправляться повторно і створять дублікат запису.
// Так робити НЕ МОЖНА
public function store(StorePostRequest $request)
{
Post::create($request->validated());
return view('posts.success'); // POST залишається в історії браузера
}
Що відбувається:
1. Користувач заповнює форму
2. POST /posts → 200 OK (сторінка "Успішно!")
3. Користувач натискає F5
4. Браузер: "Повторити відправку форми?"
5. Користувач натискає "Так"
6. Повторний POST → дублікат замовлення / подвійне списання / повторна реєстрація
POST-запит залишається останнім в історії браузера, тому будь-яке оновлення сторінки повторює його.
Рішення — PRG
Після POST не повертай view, а зроби redirect. Браузер виконає GET на нову адресу, і саме GET стане останнім запитом в історії.
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
return redirect()
->route('posts.show', $post)
->with('status', 'Пост створено!');
}
Що відбувається:
1. Користувач заповнює форму
2. POST /posts → 302 Redirect (не сторінка, а перенаправлення)
3. GET /posts/15 → 200 OK (сторінка посту)
4. Користувач натискає F5
5. Браузер повторює GET /posts/15 → безпечно, без попередження
Три кроки: Post (відправка) → Redirect (перенаправлення) → Get (відображення результату).
Flash-повідомлення
Redirect — це новий запит, тому змінні не передаються. Для одноразових повідомлень є flash-дані через ->with(). Вони живуть рівно один запит і потім зникають.
return redirect()
->route('posts.index')
->with('status', 'Пост створено!')
->with('type', 'success');
В Blade:
@if (session('status'))
<div class="alert alert-{{ session('type', 'info') }}">
{{ session('status') }}
</div>
@endif
Після оновлення сторінки повідомлення зникне — це очікувана поведінка.
Варіанти redirect після POST
Redirect на сторінку створеного ресурсу
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
return redirect()->route('posts.show', $post);
}
// POST /posts → 302 → GET /posts/15
Redirect на список
public function store(StorePostRequest $request)
{
Post::create($request->validated());
return redirect()
->route('posts.index')
->with('status', 'Пост створено!');
}
// POST /posts → 302 → GET /posts
Redirect назад (на форму)
public function store(StorePostRequest $request)
{
Post::create($request->validated());
return redirect()
->back()
->with('status', 'Збережено!');
}
// POST /posts → 302 → GET /posts/create
Redirect після оновлення
public function update(UpdatePostRequest $request, Post $post)
{
$post->update($request->validated());
return redirect()
->route('posts.show', $post)
->with('status', 'Пост оновлено!');
}
// PUT /posts/15 → 302 → GET /posts/15
Redirect після видалення
public function destroy(Post $post)
{
$post->delete();
return redirect()
->route('posts.index')
->with('status', 'Пост видалено!');
}
// DELETE /posts/15 → 302 → GET /posts
Redirect з помилками валідації
Laravel автоматично застосовує PRG при невдалій валідації: робить redirect назад з помилками і старими даними форми.
// FormRequest робить це автоматично
// Але якщо валідуєш вручну:
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|max:255',
'body' => 'required',
]);
// Якщо валідація не пройшла:
// redirect()->back()->withErrors($errors)->withInput()
// Це відбувається автоматично
Post::create($validated);
return redirect()->route('posts.index');
}
В Blade старі значення полів відновлюються через old():
<form method="POST" action="/posts">
@csrf
<input name="title" value="{{ old('title') }}">
@error('title')
<span class="error">{{ $message }}</span>
@enderror
<textarea name="body">{{ old('body') }}</textarea>
@error('body')
<span class="error">{{ $message }}</span>
@enderror
<button type="submit">Створити</button>
</form>
Потік: користувач заповнив форму → POST → валідація не пройшла → redirect назад з помилками → форма показує старі значення і повідомлення про помилки.
Повний приклад CRUD з PRG
class PostController extends Controller
{
public function index()
{
// GET — просто показуємо список
$posts = Post::latest()->paginate(10);
return view('posts.index', compact('posts'));
}
public function create()
{
// GET — показуємо форму
return view('posts.create');
}
public function store(StorePostRequest $request)
{
// POST → Redirect → Get
$post = Post::create($request->validated());
return redirect()->route('posts.show', $post)->with('status', 'Створено!');
}
public function show(Post $post)
{
// GET — показуємо пост
return view('posts.show', compact('post'));
}
public function edit(Post $post)
{
// GET — показуємо форму редагування
return view('posts.edit', compact('post'));
}
public function update(UpdatePostRequest $request, Post $post)
{
// PUT → Redirect → Get
$post->update($request->validated());
return redirect()->route('posts.show', $post)->with('status', 'Оновлено!');
}
public function destroy(Post $post)
{
// DELETE → Redirect → Get
$post->delete();
return redirect()->route('posts.index')->with('status', 'Видалено!');
}
}
Кожен метод, що змінює дані (store, update, destroy), закінчується redirect(), а не view().
Хелпери для redirect
// Повна форма
return redirect()->route('posts.show', $post);
// Короткий запис (Laravel 9+)
return to_route('posts.show', $post);
// На URL
return redirect('/posts');
// На дію контролера
return redirect()->action([PostController::class, 'index']);
// Назад
return redirect()->back();
// З кількома flash-значеннями
return redirect()->route('posts.index')->with([
'status' => 'Збережено!',
'post_id' => $post->id,
]);
Правило
Після будь-якого POST, PUT, PATCH, DELETE завжди роби redirect(), ніколи view(). Це PRG-паттерн — він захищає від дублікатів і прибирає попередження браузера про повторну відправку.