PRG-паттерн (Post → Redirect → Get) у Laravel
Glossary overview

PRG-паттерн (Post → Redirect → Get) у Laravel

Проблема

Після відправки 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-паттерн — він захищає від дублікатів і прибирає попередження браузера про повторну відправку.