BAB 7: Controllers

Mengorganisir logika request-response menggunakan controller class di Laravel.

Di bab sebelumnya, middleware bertugas sebagai pos pemeriksaan — memutuskan apakah request layak diteruskan atau harus dihentikan. Setelah request melewati semua lapisan itu, ia akhirnya sampai ke controller. Selama ini kita sudah berkali-kali menyebut controller di contoh rute, tapi hanya sebagai tujuan akhir yang belum pernah kita buka isinya.

Controller adalah tempat logika utama aplikasi tinggal. Di sinilah request diterima, data diambil dari database, dan response disiapkan untuk dikembalikan ke browser. Tanpa controller, semua logika itu akan bertumpuk di file routes/web.php dan membuat routing jadi kacau.

Membuat Controller

Laravel menyediakan perintah Artisan untuk membuat controller baru:

php artisan make:controller CatatanController

Perintah ini membuat file app/Http/Controllers/CatatanController.php. Struktur defaultnya sederhana:

<?php
// app/Http/Controllers/CatatanController.php

namespace App\Http\Controllers;

class CatatanController extends Controller
{
    //
}

Semua controller di Laravel ditempatkan di namespace App\Http\Controllers dan biasanya mengextend class Controller bawaan framework — meski ini bukan keharusan teknis, hanya konvensi.

Untuk menambahkan method pertama, tulis function di dalam class tersebut:

<?php
// app/Http/Controllers/CatatanController.php

namespace App\Http\Controllers;

use App\Models\Catatan;
use Illuminate\View\View;

class CatatanController extends Controller
{
    public function index(): View
    {
        $catatanList = Catatan::latest()->get();

        return view('catatan.index', compact('catatanList'));
    }
}

Lalu daftarkan method ini ke rute:

// routes/web.php

use App\Http\Controllers\CatatanController;

Route::get('/catatan', [CatatanController::class, 'index']);

Ketika URL /catatan dikunjungi, Laravel memanggil method index() di CatatanController. Method mengambil semua catatan dari database dan mengirimnya ke view.

Resource Controller

Ketika membangun fitur CRUD, setiap resource biasanya butuh tujuh operasi yang sama: menampilkan daftar, form tambah, simpan, tampil detail, form edit, update, dan hapus. Laravel memiliki konvensi untuk ini — disebut resource controller.

Buat resource controller dengan flag --resource:

php artisan make:controller CatatanController --resource

Controller yang dihasilkan sudah memiliki tujuh method yang terisi skeleton:

<?php
// app/Http/Controllers/CatatanController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class CatatanController extends Controller
{
    // Menampilkan daftar semua catatan
    public function index() {}

    // Menampilkan form untuk membuat catatan baru
    public function create() {}

    // Menyimpan catatan baru ke database
    public function store(Request $request) {}

    // Menampilkan satu catatan berdasarkan ID
    public function show(string $id) {}

    // Menampilkan form edit catatan
    public function edit(string $id) {}

    // Menyimpan perubahan catatan ke database
    public function update(Request $request, string $id) {}

    // Menghapus catatan dari database
    public function destroy(string $id) {}
}

Daftarkan semua tujuh rute sekaligus dengan satu baris:

// routes/web.php

use App\Http\Controllers\CatatanController;

Route::resource('catatan', CatatanController::class);

Satu baris ini menghasilkan tujuh rute yang terpetakan ke tujuh method di atas:

Method HTTPURLMethod ControllerNama Rute
GET/catatanindexcatatan.index
GET/catatan/createcreatecatatan.create
POST/catatanstorecatatan.store
GET/catatan/{catatan}showcatatan.show
GET/catatan/{catatan}/editeditcatatan.edit
PUT/PATCH/catatan/{catatan}updatecatatan.update
DELETE/catatan/{catatan}destroycatatan.destroy

Nama rute ini berguna di view untuk membuat link dan form action tanpa hardcode URL:

// Di Blade template
<a href="{{ route('catatan.index') }}">Semua Catatan</a>
<a href="{{ route('catatan.edit', $catatan) }}">Edit</a>

Membatasi Method yang Dihasilkan

Tidak selalu semua tujuh method dibutuhkan. Gunakan only() untuk memilih yang diperlukan, atau except() untuk mengecualikan yang tidak perlu:

// routes/web.php

// Hanya tampil dan detail — tidak ada form tambah/edit/hapus
Route::resource('catatan', CatatanController::class)->only([
    'index', 'show'
]);

// Semua kecuali create dan edit (biasanya untuk API)
Route::resource('catatan', CatatanController::class)->except([
    'create', 'edit'
]);

API Resource Controller

Untuk API, method create dan edit tidak dibutuhkan karena API tidak mengembalikan form HTML. Buat controller khusus API dengan flag --api:

php artisan make:controller Api/CatatanController --api

Atau gunakan apiResource() saat mendaftarkan rute:

// routes/api.php

Route::apiResource('catatan', CatatanController::class);

Single Action Controller

Terkadang sebuah controller hanya butuh satu action — misalnya controller untuk proses pembayaran, ekspor data, atau halaman landing khusus. Untuk kasus ini, gunakan invokable controller dengan method __invoke():

php artisan make:controller EksporCatatanController --invokable
<?php
// app/Http/Controllers/EksporCatatanController.php

namespace App\Http\Controllers;

use App\Models\Catatan;
use Illuminate\Http\Response;

class EksporCatatanController extends Controller
{
    public function __invoke(): Response
    {
        $catatan = Catatan::all();

        $isi = $catatan->map(fn($c) => $c->judul . ': ' . $c->isi)->join("\n");

        return response($isi)
            ->header('Content-Type', 'text/plain')
            ->header('Content-Disposition', 'attachment; filename="catatan.txt"');
    }
}

Saat mendaftarkan rute, tidak perlu menyebut nama method — langsung gunakan nama class saja:

// routes/web.php

use App\Http\Controllers\EksporCatatanController;

Route::get('/catatan/ekspor', EksporCatatanController::class);

Dependency Injection di Controller

Laravel memiliki service container yang bisa otomatis menyuntikkan dependency ke controller. Ini membuat controller mudah diuji karena dependensinya bisa diganti saat testing.

Constructor Injection

Suntikkan dependency di constructor agar tersedia di seluruh method controller:

<?php
// app/Http/Controllers/CatatanController.php

namespace App\Http\Controllers;

use App\Repositories\CatatanRepository;

class CatatanController extends Controller
{
    public function __construct(
        private CatatanRepository $catatanRepo
    ) {}

    public function index()
    {
        $catatanList = $this->catatanRepo->semuaCatatan();
        return view('catatan.index', compact('catatanList'));
    }

    public function show(string $id)
    {
        $catatan = $this->catatanRepo->cariById($id);
        return view('catatan.show', compact('catatan'));
    }
}

Method Injection

Dependency juga bisa disuntikkan langsung ke parameter method. Ini berguna untuk dependency yang hanya dibutuhkan satu method tertentu:

<?php
// app/Http/Controllers/CatatanController.php

namespace App\Http\Controllers;

use App\Models\Catatan;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class CatatanController extends Controller
{
    public function store(Request $request): RedirectResponse
    {
        $catatan = Catatan::create([
            'judul' => $request->judul,
            'isi'   => $request->isi,
        ]);

        return redirect()->route('catatan.show', $catatan)
            ->with('sukses', 'Catatan berhasil disimpan.');
    }
}

Ketika rute memiliki parameter, tambahkan di parameter method setelah dependency:

// routes/web.php
Route::put('/catatan/{id}', [CatatanController::class, 'update']);

// Controller
public function update(Request $request, string $id): RedirectResponse
{
    $catatan = Catatan::findOrFail($id);
    $catatan->update([
        'judul' => $request->judul,
        'isi'   => $request->isi,
    ]);

    return redirect()->route('catatan.show', $catatan)
        ->with('sukses', 'Catatan berhasil diperbarui.');
}

Laravel tahu untuk meneruskan $request sebagai injeksi dan $id dari parameter rute — keduanya bisa hadir bersamaan di satu method.

Middleware di Controller

Di bab sebelumnya kita menerapkan middleware langsung di rute. Alternatifnya, middleware bisa didefinisikan di dalam controller menggunakan interface HasMiddleware:

<?php
// app/Http/Controllers/CatatanController.php

namespace App\Http\Controllers;

use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;

class CatatanController extends Controller implements HasMiddleware
{
    public static function middleware(): array
    {
        return [
            'auth',
            new Middleware('verified', only: ['store', 'update', 'destroy']),
        ];
    }

    // method-method controller...
}

Dengan cara ini, semua konfigurasi middleware untuk controller terkumpul di satu tempat. Middleware auth berlaku untuk semua method, sementara verified hanya berlaku untuk operasi yang mengubah data.

Pilih di mana mendefinisikan middleware berdasarkan scope kebutuhannya. Middleware di rute cocok ketika satu middleware digunakan di banyak controller yang berbeda. Middleware di controller cocok ketika konfigurasi middleware spesifik untuk controller itu saja.

Route Model Binding

Saat controller menerima ID dari URL, pola umumnya adalah memanggil findOrFail() secara manual. Laravel memiliki cara yang lebih ringkas: route model binding.

Alih-alih menerima string $id dan mencari model secara manual, deklarasikan model langsung sebagai tipe parameter:

<?php
// app/Http/Controllers/CatatanController.php

namespace App\Http\Controllers;

use App\Models\Catatan;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

class CatatanController extends Controller
{
    // Tanpa route model binding — manual
    public function showManual(string $id): View
    {
        $catatan = Catatan::findOrFail($id); // bisa 404 otomatis
        return view('catatan.show', compact('catatan'));
    }

    // Dengan route model binding — otomatis
    public function show(Catatan $catatan): View
    {
        return view('catatan.show', compact('catatan'));
    }

    public function edit(Catatan $catatan): View
    {
        return view('catatan.edit', compact('catatan'));
    }

    public function update(Request $request, Catatan $catatan): RedirectResponse
    {
        $catatan->update($request->only(['judul', 'isi']));
        return redirect()->route('catatan.show', $catatan);
    }

    public function destroy(Catatan $catatan): RedirectResponse
    {
        $catatan->delete();
        return redirect()->route('catatan.index')
            ->with('sukses', 'Catatan berhasil dihapus.');
    }
}

Laravel mencocokkan nama parameter di URL dengan nama parameter di method controller. Ketika URL /catatan/{catatan} dipanggil dengan ID 5, Laravel otomatis mencari Catatan::find(5). Jika tidak ditemukan, Laravel langsung mengembalikan response 404 — tanpa satu baris kode tambahan.

Nama parameter di rute harus cocok dengan nama parameter di method controller. Rute {catatan} akan cocok dengan Catatan $catatan, bukan Catatan $id. Pastikan keduanya konsisten.

Nested Resource Controller

Ketika resource memiliki hubungan parent-child — misalnya komentar yang dimiliki oleh catatan — gunakan nested resource:

php artisan make:controller KomentarController --resource
// routes/web.php

Route::resource('catatan.komentar', KomentarController::class);

Ini menghasilkan URL seperti /catatan/{catatan}/komentar/{komentar}. Di controller, kedua model bisa diterima sekaligus:

<?php
// app/Http/Controllers/KomentarController.php

namespace App\Http\Controllers;

use App\Models\Catatan;
use App\Models\Komentar;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

class KomentarController extends Controller
{
    public function index(Catatan $catatan): View
    {
        return view('komentar.index', [
            'catatan'      => $catatan,
            'komentarList' => $catatan->komentar()->latest()->get(),
        ]);
    }

    public function store(Request $request, Catatan $catatan): RedirectResponse
    {
        $catatan->komentar()->create([
            'isi'     => $request->isi,
            'user_id' => $request->user()->id,
        ]);

        return redirect()->route('catatan.komentar.index', $catatan)
            ->with('sukses', 'Komentar ditambahkan.');
    }

    public function destroy(Catatan $catatan, Komentar $komentar): RedirectResponse
    {
        $komentar->delete();
        return redirect()->route('catatan.komentar.index', $catatan);
    }
}

Latihan

Coba kerjakan latihan berikut menggunakan konsep yang sudah dipelajari di bab ini dan bab-bab sebelumnya:

  1. Buat resource controller — Buat TagController dengan --resource. Implementasikan method index() yang mengambil semua tag dari database dan mengirimnya ke view tag/index.blade.php. Daftarkan rutenya dan pastikan halaman bisa diakses.

  2. Route model binding — Ubah method show() dan edit() di CatatanController agar menggunakan route model binding. Verifikasi bahwa mengakses ID yang tidak ada secara otomatis menghasilkan halaman 404.

  3. Invokable controller — Buat RingkasanController yang mengimplementasikan __invoke() untuk menghitung dan menampilkan statistik: total catatan, catatan hari ini, dan catatan bulan ini. Daftarkan di rute /ringkasan.

Penutup Bab

Controller adalah penghubung antara rute dan logika aplikasi. Dengan resource controller, tujuh operasi CRUD bisa terdaftar dan terorganisir hanya dengan beberapa baris kode. Route model binding menghilangkan boilerplate pencarian dan penanganan 404 secara manual. Dependency injection membuat controller tetap bersih dan mudah diuji.

Semua yang masuk ke controller datang dalam bentuk Request — sebuah objek yang menyimpan seluruh informasi dari request HTTP: input form, query string, file upload, header, hingga cookie. Bab berikutnya akan membedah objek Request ini secara menyeluruh, sehingga kamu tahu persis cara mengakses data apapun yang dikirim user ke aplikasi.

Referensi

  1. 1Controllers — Laravel 12.x Documentation
  2. 2Route Model Binding — Laravel 12.x Documentation