Di bagian sebelumnya kita sudah menguasai routing — cara Laravel mencocokkan URL dengan kode yang harus dijalankan. Sekarang saatnya membahas Controller: tempat di mana logika aplikasimu sebenarnya berada. Controller adalah jembatan antara route dan tampilan — ia menerima request, memproses data yang diperlukan, dan mengirimkan respons yang tepat kembali ke pengguna.

Panduan

Alur Request MVC

Route

Menentukan URL dan method controller yang dipanggil.

#
Controller

Mengambil atau menyiapkan data yang dibutuhkan halaman.

#
View

Menampilkan data ke user dalam bentuk HTML.

💡 Ini adalah bagian ketiga dari seri "Belajar Laravel dari Nol". Pastikan kamu sudah menyelesaikan Bagian 1 (Instalasi) dan Bagian 2 (Routing) sebelum melanjutkan, karena bagian ini membangun langsung di atas konsep routing yang sudah dipelajari.


Apa Itu Controller dan Mengapa Dibutuhkan?

Saat belajar routing di bagian sebelumnya, kita sempat mendefinisikan logika langsung di dalam closure route menggunakan function() { ... }. Cara ini bisa bekerja untuk route yang sangat sederhana, tapi akan menjadi masalah seiring aplikasi berkembang — file routes/web.php akan menjadi sangat panjang, kode sulit di-test, dan tidak bisa digunakan ulang. Controller hadir sebagai solusi: memindahkan logika ke kelas PHP terpisah yang terorganisir dan mudah di-maintain.

"Fat models, skinny controllers — keep your controllers lean and focused on handling HTTP requests, nothing more."

— — Prinsip arsitektur Laravel

Membuat Controller dengan Artisan

Cara terbaik dan tercepat membuat Controller di Laravel adalah menggunakan Artisan CLI. Artisan tidak hanya membuat file di lokasi yang tepat, tapi juga mengisi boilerplate kode dasar yang dibutuhkan — namespace, use statement, dan extends Controller — sehingga kamu bisa langsung fokus menulis logika.

ContohController.blade.php
Bash
# ── BERBAGAI CARA MEMBUAT CONTROLLER ─────────────────

# Controller kosong (tanpa method apapun)
php artisan make:controller PostController

# Resource Controller (7 method CRUD sudah dibuat otomatis)
php artisan make:controller PostController --resource

# Resource Controller + langsung bind ke Model
php artisan make:controller PostController --resource --model=Post

# API Controller (seperti resource tapi tanpa create & edit)
php artisan make:controller PostController --api

# Single Action Controller (hanya satu method: __invoke)
php artisan make:controller ShowDashboardController --invokable

# Controller di dalam subfolder
php artisan make:controller Admin/PostController --resource --model=Post
# File dibuat: app/Http/Controllers/Admin/PostController.php
# Namespace: App\Http\Controllers\Admin

# Lihat hasil semua file controller yang ada
ls app/Http/Controllers/

Anatomi Sebuah Controller

Mari bedah struktur dasar sebuah Controller Laravel agar kamu memahami setiap bagiannya — dari namespace, import, hingga cara method Controller terhubung dengan route dan view.

PostController.php
PHP
<?php

// Namespace menentukan lokasi file di dalam project
// Sesuai dengan struktur folder app/Http/Controllers/
namespace App\Http\Controllers;

// Import class yang dibutuhkan
use App\Models\Post;           // Model untuk interaksi database
use Illuminate\Http\Request;   // Objek request HTTP
use Illuminate\View\View;      // Type hint untuk return view
use Illuminate\Http\RedirectResponse; // Type hint untuk redirect

// Controller harus extend base Controller Laravel
// Base Controller ada di: app/Http/Controllers/Controller.php
class PostController extends Controller
{
    /**
     * Method bisa menerima parameter yang otomatis di-inject
     * oleh Laravel Service Container:
     *
     * - Request $request → data dari HTTP request (form, query string, dll)
     * - Post $post       → Route Model Binding (otomatis cari di database)
     * - int $id          → Parameter route biasa
     */
    public function index(): View
    {
        // Ambil data dari database menggunakan Eloquent
        $posts = Post::latest()->paginate(10);

        // Kirim data ke view
        // view('nama.view', ['variabel' => nilai])
        return view('posts.index', compact('posts'));
    }

    public function show(Post $post): View
    {
        // $post sudah otomatis diisi oleh Route Model Binding
        return view('posts.show', compact('post'));
    }
}

Resource Controller: Tujuh Method CRUD

Resource Controller adalah jenis Controller yang paling sering digunakan di Laravel. Ia mengikuti konvensi RESTful dengan tujuh method yang sudah memiliki peran yang jelas dan terprediksi. Memahami tujuh method ini — apa fungsinya, kapan dijalankan, dan apa yang harus dikembalikan — adalah kunci produktivitas sebagai Laravel developer.

#
index(): Tampilkan Semua Data

Dipanggil saat request GET /posts. Bertugas mengambil daftar semua record dari database (biasanya dengan pagination) dan mengirimkannya ke view untuk ditampilkan sebagai daftar atau tabel. Ini adalah halaman "list" dari sebuah fitur.

#
create(): Tampilkan Form Tambah Data

Dipanggil saat request GET /posts/create. Bertugas menampilkan form kosong untuk membuat data baru. Method ini biasanya tidak berinteraksi dengan database — ia hanya mengembalikan view yang berisi HTML form. Data yang dibutuhkan form (seperti daftar kategori untuk dropdown) dikirim dari sini.

#
store(): Simpan Data Baru

Dipanggil saat request POST /posts (form create di-submit). Bertugas memvalidasi data dari form, menyimpannya ke database, lalu melakukan redirect ke halaman yang sesuai (biasanya ke halaman show atau index) dengan flash message sukses.

#
show(): Tampilkan Detail Satu Data

Dipanggil saat request GET /posts/{post}. Bertugas mengambil satu record spesifik dari database berdasarkan parameter di URL dan menampilkan detail lengkapnya. Dengan Route Model Binding, record sudah otomatis tersedia sebagai parameter.

#
edit(): Tampilkan Form Edit Data

Dipanggil saat request GET /posts/{post}/edit. Mirip dengan create(), tapi menampilkan form yang sudah terisi dengan data yang ada untuk diedit. Record yang akan diedit dikirim ke view agar form bisa menampilkan nilai saat ini.

#
update(): Simpan Perubahan Data

Dipanggil saat request PUT atau PATCH /posts/{post} (form edit di-submit). Bertugas memvalidasi data baru, mengupdate record yang ada di database, lalu redirect dengan flash message sukses.

#
destroy(): Hapus Data

Dipanggil saat request DELETE /posts/{post}. Bertugas menghapus record dari database lalu redirect ke halaman index dengan flash message konfirmasi penghapusan.

PostController.php
PHP
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;

class PostController extends Controller
{
    /**
     * GET /posts
     * Tampilkan daftar semua post
     */
    public function index(): View
    {
        $posts = Post::with('category', 'user')
            ->latest()
            ->paginate(10);

        return view('posts.index', compact('posts'));
    }

    /**
     * GET /posts/create
     * Tampilkan form untuk membuat post baru
     */
    public function create(): View
    {
        // Kirim data yang dibutuhkan form (contoh: daftar kategori)
        $categories = Category::orderBy('name')->get();

        return view('posts.create', compact('categories'));
    }

    /**
     * POST /posts
     * Simpan post baru ke database
     */
    public function store(Request $request): RedirectResponse
    {
        // Validasi data dari form
        $validated = $request->validate([
            'title'       => 'required|string|max:255',
            'content'     => 'required|string',
            'category_id' => 'required|exists:categories,id',
            'image'       => 'nullable|image|max:2048',
        ]);

        // Simpan ke database
        $post = Post::create([
            'title'       => $validated['title'],
            'content'     => $validated['content'],
            'category_id' => $validated['category_id'],
            'user_id'     => auth()->id(),
            'slug'        => str($validated['title'])->slug(),
        ]);

        // Redirect dengan flash message
        return redirect()
            ->route('posts.show', $post)
            ->with('success', 'Post berhasil dibuat!');
    }

    /**
     * GET /posts/{post}
     * Tampilkan detail satu post
     */
    public function show(Post $post): View
    {
        // Load relasi yang dibutuhkan
        $post->load('category', 'user', 'comments.user');

        return view('posts.show', compact('post'));
    }

    /**
     * GET /posts/{post}/edit
     * Tampilkan form edit post
     */
    public function edit(Post $post): View
    {
        $categories = Category::orderBy('name')->get();

        return view('posts.edit', compact('post', 'categories'));
    }

    /**
     * PUT/PATCH /posts/{post}
     * Update post yang ada di database
     */
    public function update(Request $request, Post $post): RedirectResponse
    {
        $validated = $request->validate([
            'title'       => 'required|string|max:255',
            'content'     => 'required|string',
            'category_id' => 'required|exists:categories,id',
        ]);

        $post->update([
            'title'       => $validated['title'],
            'content'     => $validated['content'],
            'category_id' => $validated['category_id'],
            'slug'        => str($validated['title'])->slug(),
        ]);

        return redirect()
            ->route('posts.show', $post)
            ->with('success', 'Post berhasil diperbarui!');
    }

    /**
     * DELETE /posts/{post}
     * Hapus post dari database
     */
    public function destroy(Post $post): RedirectResponse
    {
        $post->delete();

        return redirect()
            ->route('posts.index')
            ->with('success', 'Post berhasil dihapus!');
    }
}

Mengirim Data dari Controller ke View

Salah satu tugas utama Controller adalah menyiapkan data dan mengirimkannya ke View untuk ditampilkan. Laravel menyediakan beberapa cara untuk melakukan ini — semuanya valid, pilih yang paling mudah dibaca sesuai konteks dan preferensi timmu.

contohFUnction.php
PHP
<?php

// ── CARA 1: Array asosiatif (cara paling eksplisit) ──
public function index(): View
{
    $posts = Post::all();
    $total = Post::count();

    return view('posts.index', [
        'posts' => $posts,
        'total' => $total,
        'judul' => 'Semua Artikel',
    ]);
}

// ── CARA 2: compact() (paling umum digunakan) ─────────
// compact() membuat array dari nama variabel yang disebutkan
public function index(): View
{
    $posts = Post::all();
    $total = Post::count();
    $judul = 'Semua Artikel';

    // compact('posts', 'total', 'judul') sama dengan:
    // ['posts' => $posts, 'total' => $total, 'judul' => $judul]
    return view('posts.index', compact('posts', 'total', 'judul'));
}

// ── CARA 3: with() method chaining ───────────────────
public function index(): View
{
    return view('posts.index')
        ->with('posts', Post::all())
        ->with('total', Post::count())
        ->with('judul', 'Semua Artikel');
}

// ── CARA 4: with() dengan array ───────────────────────
public function index(): View
{
    return view('posts.index')->with([
        'posts' => Post::all(),
        'total' => Post::count(),
        'judul' => 'Semua Artikel',
    ]);
}

Redirect dari Controller

Tidak semua method Controller harus mengembalikan view. Method yang memproses form (store, update, destroy) biasanya mengembalikan redirect — mengarahkan pengguna ke halaman lain setelah operasi selesai. Laravel menyediakan helper redirect() yang sangat fleksibel dengan berbagai opsi.

PHP
<?php

// ── REDIRECT KE NAMED ROUTE ───────────────────────────
return redirect()->route('posts.index');
return redirect()->route('posts.show', ['post' => $post->id]);
return redirect()->route('posts.show', $post); // shorthand dengan model

// ── REDIRECT DENGAN FLASH MESSAGE ────────────────────
// Flash message tersimpan di session untuk satu request berikutnya
return redirect()
    ->route('posts.index')
    ->with('success', 'Post berhasil disimpan!');

return redirect()
    ->route('posts.index')
    ->with('error', 'Terjadi kesalahan, coba lagi.');

return redirect()
    ->route('posts.index')
    ->with('info', 'Data sedang diproses.');

// ── REDIRECT KEMBALI (ke halaman sebelumnya) ──────────
// Berguna saat validasi gagal
return redirect()->back();
return redirect()->back()->with('error', 'Validasi gagal!');

// ── REDIRECT DENGAN INPUT LAMA (untuk isi ulang form) ─
// withInput() menyimpan input ke session agar form bisa diisi ulang
return redirect()
    ->back()
    ->withInput()
    ->withErrors(['title' => 'Judul sudah digunakan.']);

// ── REDIRECT KE URL LANGSUNG ──────────────────────────
return redirect('/posts');
return redirect('https://external-site.com');

// ── REDIRECT KE ACTION CONTROLLER ────────────────────
return redirect()->action([PostController::class, 'index']);

Menerima Data dari Request

Setiap request yang masuk ke Controller membawa data — bisa dari form input, query string di URL, parameter route, atau JSON body dari API call. Objek Request yang di-inject ke method Controller adalah pintu akses ke semua data tersebut.

contohRequest.php
PHP
<?php

use Illuminate\Http\Request;

public function store(Request $request): RedirectResponse
{
    // ── MENGAMBIL INPUT DARI FORM / REQUEST BODY ──────
    $title   = $request->input('title');       // Cara paling umum
    $content = $request->content;              // Magic property (sama saja)
    $title   = $request->input('title', 'Untitled'); // Dengan default value

    // Ambil semua input sekaligus
    $allInput = $request->all();
    // ['title' => '...', 'content' => '...', '_token' => '...']

    // Ambil hanya field tertentu (lebih aman)
    $data = $request->only(['title', 'content', 'category_id']);

    // Ambil semua kecuali field tertentu
    $data = $request->except(['_token', '_method']);

    // ── QUERY STRING (?page=2&sort=latest) ───────────
    $page = $request->query('page', 1);        // Default: 1
    $sort = $request->query('sort', 'latest');

    // ── CEK KEBERADAAN INPUT ──────────────────────────
    if ($request->has('title')) {
        // Field 'title' ada di request
    }

    if ($request->filled('title')) {
        // Field 'title' ada DAN tidak kosong/null
    }

    if ($request->missing('title')) {
        // Field 'title' tidak ada di request
    }

    // ── INFORMASI REQUEST ─────────────────────────────
    $method = $request->method();         // GET, POST, PUT, DELETE, dll
    $url    = $request->url();            // http://localhost:8000/posts
    $path   = $request->path();           // posts
    $ip     = $request->ip();             // IP address pengguna
    $isAjax = $request->ajax();           // true jika AJAX request
    $isJson = $request->wantsJson();      // true jika expect JSON response

    // ── FILE UPLOAD ───────────────────────────────────
    if ($request->hasFile('image')) {
        $file     = $request->file('image');
        $filename = $file->getClientOriginalName();
        $size     = $file->getSize();
        $path     = $file->store('images', 'public');
        // Tersimpan di storage/app/public/images/
    }
}

💡 Selalu gunakan $request->only() atau $request->validated() (setelah validasi) daripada $request->all() untuk mengambil data yang akan disimpan ke database. Ini mencegah mass assignment vulnerability — pengguna yang iseng menambahkan field ekstra di request tidak bisa mengisi kolom yang tidak seharusnya mereka isi, seperti is_admin atau role.


Validasi Data di Controller

Validasi adalah salah satu hal paling penting dalam setiap aplikasi web — memastikan data yang masuk dari pengguna sesuai dengan format dan aturan yang diharapkan sebelum diproses atau disimpan ke database. Laravel menyediakan sistem validasi yang sangat lengkap dan ekspresif langsung di dalam Controller.

#
Validasi Langsung di Controller

Cara paling sederhana adalah menggunakan method $request->validate() langsung di dalam Controller. Jika validasi gagal, Laravel otomatis redirect kembali ke halaman sebelumnya dengan error messages dan input lama tersimpan di session — tanpa perlu kode tambahan.

#
404.blade.php
BLADE
{{-- Menampilkan error validasi di Blade template --}}

{{-- Cek satu field tertentu --}}
<div>
    <label>Judul</label>
    <input
        type="text"
        name="title"
        value="{{ old('title') }}"
        class="{{ $errors->has('title') ? 'border-red-500' : '' }}"
    >

    @error('title')
        <p class="text-red-500 text-sm mt-1">{{ $message }}</p>
    @enderror
</div>

{{-- Tampilkan semua error sekaligus di atas form --}}
@if ($errors->any())
    <div class="bg-red-50 border border-red-200 rounded p-4 mb-4">
        <p class="font-semibold text-red-700">Terdapat beberapa kesalahan:</p>
        <ul class="list-disc list-inside mt-2">
            @foreach ($errors->all() as $error)
                <li class="text-red-600 text-sm">{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

{{-- Flash message dari redirect()->with() --}}
@if (session('success'))
    <div class="bg-green-50 border border-green-200 rounded p-4 mb-4">
        <p class="text-green-700">{{ session('success') }}</p>
    </div>
@endif

@if (session('error'))
    <div class="bg-red-50 border border-red-200 rounded p-4 mb-4">
        <p class="text-red-700">{{ session('error') }}</p>
    </div>
@endif
Menampilkan Error di Blade

Error validasi otomatis tersedia di semua Blade template melalui variabel $errors. Gunakan $errors->has('field') untuk mengecek apakah ada error untuk field tertentu, dan $errors->first('field') untuk mengambil pesan error pertama.

#
storeRequest.php
PHP
<?php

public function store(Request $request): RedirectResponse
{
    // validate() otomatis redirect back + withErrors + withInput
    // jika validasi gagal
    $validated = $request->validate([
        // ── STRING ────────────────────────────────────
        'title'    => 'required|string|min:5|max:255',
        'slug'     => 'required|string|unique:posts,slug',
        'excerpt'  => 'nullable|string|max:500',
        'content'  => 'required|string',

        // ── ANGKA ─────────────────────────────────────
        'price'    => 'required|numeric|min:0',
        'stock'    => 'required|integer|min:0|max:9999',
        'rating'   => 'required|decimal:0,1|between:0,5',

        // ── RELASI / FOREIGN KEY ──────────────────────
        'category_id' => 'required|exists:categories,id',
        'tag_ids'     => 'nullable|array',
        'tag_ids.*'   => 'exists:tags,id',

        // ── TANGGAL ────────────────────────────────────
        'published_at' => 'nullable|date|after:today',
        'expired_at'   => 'nullable|date|after:published_at',

        // ── FILE ───────────────────────────────────────
        'image'    => 'nullable|image|mimes:jpg,png,webp|max:2048',
        'document' => 'nullable|file|mimes:pdf,doc,docx|max:5120',

        // ── BOOLEAN ────────────────────────────────────
        'is_featured' => 'boolean',
        'is_active'   => 'required|boolean',

        // ── EMAIL & URL ────────────────────────────────
        'email'    => 'required|email|unique:users,email',
        'website'  => 'nullable|url',

        // ── PASSWORD ───────────────────────────────────
        'password' => 'required|string|min:8|confirmed',
        // 'confirmed' → butuh field 'password_confirmation' di form
    ]);

    // $validated hanya berisi field yang lolos validasi
    Post::create($validated);

    return redirect()->route('posts.index')
        ->with('success', 'Post berhasil disimpan!');
}
Aturan Validasi yang Paling Sering Dipakai

Laravel menyediakan puluhan aturan validasi bawaan. Menguasai aturan-aturan yang paling umum akan membuatmu bisa menangani hampir semua kebutuhan validasi form tanpa perlu menulis logika validasi manual.


Single Action Controller

Tidak semua fitur cocok dimasukkan ke dalam Resource Controller yang memiliki tujuh method. Untuk aksi yang berdiri sendiri dan tidak memiliki banyak variasi — seperti halaman dashboard, proses export data, atau handler webhook — Laravel menyediakan Single Action Controller yang hanya memiliki satu method: __invoke().

SingleActionController.php
PHP
<?php
// ── BUAT SINGLE ACTION CONTROLLER ────────────────────
// php artisan make:controller ShowDashboardController --invokable

namespace App\Http\Controllers;

use Illuminate\View\View;

class ShowDashboardController extends Controller
{
    /**
     * Method __invoke() dipanggil saat Controller ini digunakan
     * Tidak perlu menyebut nama method di route definition
     */
    public function __invoke(): View
    {
        $stats = [
            'total_posts'    => Post::count(),
            'total_users'    => User::count(),
            'total_comments' => Comment::count(),
            'recent_posts'   => Post::latest()->take(5)->get(),
        ];

        return view('dashboard', compact('stats'));
    }
}
routes/web.php
PHP
<?php
// routes/web.php — Menggunakan Single Action Controller

use App\Http\Controllers\ShowDashboardController;
use App\Http\Controllers\ExportPostsController;
use App\Http\Controllers\HandleStripeWebhookController;

// Tidak perlu @method karena hanya ada __invoke()
Route::get('/dashboard', ShowDashboardController::class)
    ->name('dashboard')
    ->middleware('auth');

Route::get('/posts/export', ExportPostsController::class)
    ->name('posts.export')
    ->middleware('auth');

Route::post('/webhook/stripe', HandleStripeWebhookController::class)
    ->name('webhook.stripe');

Controller Constructor: Middleware dan Dependency Injection

Constructor di Controller memiliki dua kegunaan utama yang sangat berguna: menerapkan middleware ke semua atau sebagian method Controller sekaligus, dan meng-inject dependensi yang dibutuhkan oleh seluruh Controller menggunakan Laravel Service Container.

PostController.php
PHP
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use App\Services\PostService;
use App\Repositories\PostRepository;
use Illuminate\Http\Request;

class PostController extends Controller
{
    // ── DEPENDENCY INJECTION di constructor ───────────
    // Laravel Service Container otomatis meng-inject dependensi
    public function __construct(
        private readonly PostService    $postService,
        private readonly PostRepository $postRepository,
    ) {
        // ── MIDDLEWARE DI CONSTRUCTOR ──────────────────
        // Terapkan middleware ke SEMUA method
        $this->middleware('auth');

        // Terapkan ke method tertentu SAJA
        $this->middleware('auth')->only(['create', 'store', 'edit', 'update', 'destroy']);

        // Terapkan ke SEMUA method KECUALI yang disebutkan
        $this->middleware('auth')->except(['index', 'show']);

        // Terapkan beberapa middleware
        $this->middleware(['auth', 'verified'])->only(['store', 'update', 'destroy']);
    }

    public function index()
    {
        // Gunakan service atau repository yang sudah di-inject
        $posts = $this->postRepository->getAllPublished();
        return view('posts.index', compact('posts'));
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title'   => 'required|string|max:255',
            'content' => 'required|string',
        ]);

        // Delegasikan logika bisnis ke Service
        $post = $this->postService->create($validated, auth()->user());

        return redirect()->route('posts.show', $post)
            ->with('success', 'Post berhasil dibuat!');
    }
}

💡 Jika Controller mulai terasa terlalu panjang dan penuh logika bisnis yang kompleks, itu pertanda kamu perlu memindahkan logika tersebut ke Service Class atau Repository. Controller seharusnya hanya bertugas: (1) menerima request, (2) memanggil service/model yang tepat, dan (3) mengembalikan response. Logika bisnis yang kompleks sebaiknya ada di Service Class terpisah yang bisa di-test secara independen.


Mengembalikan Response Berbeda dari Controller

Controller tidak harus selalu mengembalikan view atau redirect. Ada berbagai jenis response yang bisa dikembalikan tergantung kebutuhan — JSON untuk API, file download, response dengan status code tertentu, atau bahkan response stream untuk konten yang besar.

ContohResponse.php
PHP
<?php

public function show(Post $post)
{
    // ── RETURN VIEW (paling umum) ─────────────────────
    return view('posts.show', compact('post'));
}

public function apiShow(Post $post)
{
    // ── RETURN JSON (untuk API) ───────────────────────
    return response()->json([
        'data'    => $post,
        'message' => 'Success',
    ]);

    // Dengan status code
    return response()->json(['error' => 'Not found'], 404);
}

public function export()
{
    // ── RETURN FILE DOWNLOAD ──────────────────────────
    $pathToFile = storage_path('app/exports/posts.csv');
    return response()->download($pathToFile, 'daftar-post.csv');
}

public function preview()
{
    // ── TAMPILKAN FILE DI BROWSER (bukan download) ───
    $pathToFile = storage_path('app/documents/panduan.pdf');
    return response()->file($pathToFile);
}

public function notFound()
{
    // ── RETURN DENGAN STATUS CODE TERTENTU ────────────
    return response()->view('errors.404', [], 404);
    return response('Unauthorized', 401);
    return response('', 204); // No content (untuk DELETE API)
}

public function stream()
{
    // ── RETURN STRING / HTML LANGSUNG ────────────────
    return response('<h1>Hello World</h1>')
        ->header('Content-Type', 'text/html');
}

Praktik: Controller Lengkap untuk Blog

Mari satukan semua konsep yang sudah dipelajari dalam satu contoh Controller yang komprehensif dan mendekati kode production — lengkap dengan validasi, flash message, authorization sederhana, dan penanganan file upload.

PostController.php
PHP
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;

class PostController extends Controller
{
    public function __construct()
    {
        // Hanya yang sudah login bisa create, edit, update, delete
        $this->middleware('auth')->except(['index', 'show']);
    }

    public function index(): View
    {
        $posts = Post::with(['category', 'user'])
            ->where('status', 'published')
            ->latest('published_at')
            ->paginate(12);

        return view('posts.index', compact('posts'));
    }

    public function create(): View
    {
        $categories = Category::orderBy('name')->get();
        return view('posts.create', compact('categories'));
    }

    public function store(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'title'       => 'required|string|max:255',
            'content'     => 'required|string|min:100',
            'category_id' => 'required|exists:categories,id',
            'thumbnail'   => 'nullable|image|mimes:jpg,jpeg,png,webp|max:2048',
            'status'      => 'required|in:draft,published',
        ]);

        // Handle upload thumbnail
        $thumbnailPath = null;
        if ($request->hasFile('thumbnail')) {
            $thumbnailPath = $request->file('thumbnail')
                ->store('thumbnails', 'public');
        }

        Post::create([
            'title'       => $validated['title'],
            'slug'        => Str::slug($validated['title']),
            'content'     => $validated['content'],
            'category_id' => $validated['category_id'],
            'thumbnail'   => $thumbnailPath,
            'status'      => $validated['status'],
            'user_id'     => auth()->id(),
            'published_at'=> $validated['status'] === 'published' ? now() : null,
        ]);

        return redirect()->route('posts.index')
            ->with('success', 'Post berhasil dipublikasikan!');
    }

    public function show(Post $post): View
    {
        // Abort 404 jika post bukan published dan bukan pemiliknya
        if ($post->status !== 'published' && auth()->id() !== $post->user_id) {
            abort(404);
        }

        $post->load(['category', 'user']);
        $relatedPosts = Post::where('category_id', $post->category_id)
            ->where('id', '!=', $post->id)
            ->where('status', 'published')
            ->latest()
            ->take(3)
            ->get();

        return view('posts.show', compact('post', 'relatedPosts'));
    }

    public function edit(Post $post): View
    {
        // Pastikan hanya pemilik post yang bisa edit
        if (auth()->id() !== $post->user_id) {
            abort(403, 'Kamu tidak punya izin untuk mengedit post ini.');
        }

        $categories = Category::orderBy('name')->get();
        return view('posts.edit', compact('post', 'categories'));
    }

    public function update(Request $request, Post $post): RedirectResponse
    {
        if (auth()->id() !== $post->user_id) {
            abort(403);
        }

        $validated = $request->validate([
            'title'       => 'required|string|max:255',
            'content'     => 'required|string|min:100',
            'category_id' => 'required|exists:categories,id',
            'thumbnail'   => 'nullable|image|mimes:jpg,jpeg,png,webp|max:2048',
            'status'      => 'required|in:draft,published',
        ]);

        // Handle update thumbnail
        if ($request->hasFile('thumbnail')) {
            // Hapus thumbnail lama
            if ($post->thumbnail) {
                Storage::disk('public')->delete($post->thumbnail);
            }
            $validated['thumbnail'] = $request->file('thumbnail')
                ->store('thumbnails', 'public');
        }

        $post->update([
            ...$validated,
            'slug'        => Str::slug($validated['title']),
            'published_at'=> $validated['status'] === 'published'
                ? ($post->published_at ?? now())
                : null,
        ]);

        return redirect()->route('posts.show', $post)
            ->with('success', 'Post berhasil diperbarui!');
    }

    public function destroy(Post $post): RedirectResponse
    {
        if (auth()->id() !== $post->user_id) {
            abort(403);
        }

        // Hapus thumbnail dari storage
        if ($post->thumbnail) {
            Storage::disk('public')->delete($post->thumbnail);
        }

        $post->delete();

        return redirect()->route('posts.index')
            ->with('success', 'Post berhasil dihapus.');
    }
}

⚠️ Perhatikan pola authorization manual menggunakan abort(403) di contoh di atas. Ini adalah cara sederhana yang valid untuk project kecil. Tapi untuk project yang lebih besar dengan aturan authorization yang kompleks, Laravel menyediakan sistem Policy yang jauh lebih terstruktur dan mudah di-test. Kita akan membahas Policy secara mendalam di Bagian 10 (Middleware dan Authorization).


Langkah Selanjutnya

Luar biasa — kamu sudah menguasai Controller di Laravel secara menyeluruh! Mulai dari membuat Controller dengan Artisan, memahami tujuh method resource, mengirim data ke view, redirect dengan flash message, menerima dan memvalidasi data dari request, hingga membangun Controller lengkap yang mendekati kode production. Di bagian keempat seri ini, kita akan menyelami Blade Templating Engine — cara membuat tampilan yang dinamis, reusable, dan terorganisir dengan component, layout, dan berbagai direktif Blade yang powerful.

🚀 Sebelum lanjut ke Bagian 4, pastikan kamu sudah berlatih: (1) membuat Resource Controller dengan Artisan dan menghubungkannya ke Route::resource(), (2) mengimplementasikan semua tujuh method CRUD dengan validasi dan flash message, (3) mencoba berbagai cara mengirim data dari Controller ke View, dan (4) bereksperimen dengan redirect() dan berbagai opsinya. Semakin banyak kamu berlatih menulis Controller, semakin natural pola-pola ini akan terasa.