Belajar Laravel #4: Controller Lebih Dalam — Resource Controller & Dependency Injection

Masih ingat di episode kedua waktu pertama kali bikin controller? Waktu itu saya cuma sekadar “biar routenya rapi”. Belakangan, setelah mulai menulis logika yang lebih panjang, baru terasa bedanya: controller itu bukan sekadar tempat numpang kode, tapi otak dari aplikasi. Hari ini saya bedah lebih dalam: cara membuat controller dengan Artisan, resource controller yang menghasilkan tujuh method sekaligus, dan sedikit tentang dependency injection yang awalnya bikin saya bingung.

Tabel tujuh method resource controller Laravel beserta HTTP verb dan URI-nya
Satu baris Route::resource menghasilkan tujuh route dengan konvensi nama method yang tetap.

Kapan closure di route tidak cukup

Di episode ketiga saya sempat menulis beberapa route dengan closure langsung. Untuk halaman sederhana memang enak: satu file, satu fungsi, selesai. Tapi begitu logikanya lebih dari tiga atau empat baris — misalnya ambil data, validasi, lalu return view — route web.php cepat berubah jadi kuburan kode. Ini berarti bagi saya: closure cocok untuk halaman statis kecil, sisanya wajib pindah ke controller. Analoginya seperti dapur rumah: masak mie instan boleh di kamar pakai ketel listrik, tapi kalau sudah masak hidangan utuh, ya harus di dapur yang tertata.

Membuat controller dengan Artisan

Cara paling bersih membuat controller adalah lewat perintah Artisan. Saya suka bagian ini karena Laravel yang menyusun kerangka filenya, jadi tinggal isi logika:

php artisan make:controller TaskController

# Versi lengkap: tujuh method CRUD sekaligus
php artisan make:controller TaskController --resource

File hasilnya muncul di app/Http/Controllers/. Kebiasaan saya sekarang: setiap kali mau bikin controller, selalu lewat Artisan, bukan buat file manual. Selain konsisten dengan konvensi framework, nama class dan namespace-nya pasti benar.

Resource controller: satu route, tujuh method

Ini fitur yang paling saya suka minggu ini. Dengan satu baris Route::resource, Laravel otomatis memetakan tujuh route ke tujuh method dengan nama yang sudah dibakukan:

use App\Http\Controllers\TaskController;

Route::resource('tasks', TaskController::class);

// Setara dengan menulis manual:
// GET    /tasks            TaskController@index   - daftar data
// GET    /tasks/create     TaskController@create  - form tambah
// POST   /tasks            TaskController@store   - simpan baru
// GET    /tasks/{task}     TaskController@show    - detail satu data
// GET    /tasks/{task}/edit  TaskController@edit  - form ubah
// PUT    /tasks/{task}     TaskController@update  - simpan perubahan
// DELETE /tasks/{task}     TaskController@destroy - hapus

Yang penting dicatat: urutan pendefinisian di tabel di atas bukan asal-asalan. Route /tasks/create harus terdaftar sebelum /tasks/{task}, karena kalau tidak, kata “create” bisa tertelan sebagai nilai parameter {task}. Untung Route::resource sudah menata urutannya secara otomatis — salah satu alasan kenapa saya makin jarang mendefinisikan route CRUD secara manual.

Contoh nyata: controller daftar tugas

Saya praktikkan langsung dengan studi kasus kecil: manajemen tugas. Ini isi controller versi minimal saya:

namespace App\Http\Controllers;

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

class TaskController extends Controller
{
    public function index(): View
    {
        return view('tasks.index', [
            'tasks' => Task::latest()->get(),
        ]);
    }

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

        Task::create($validated);

        return redirect()->route('tasks.index');
    }

    public function destroy(Task $task): RedirectResponse
    {
        $task->delete();

        return redirect()->route('tasks.index');
    }
}

Dua hal yang membuat saya berdecak saat membaca ulang kode di atas. Pertama, route model binding: type-hint Task $task di method destroy membuat Laravel otomatis mencari data berdasarkan ID di URL — kalau tidak ketemu, langsung 404 tanpa saya menulis satu baris pengecekan pun. Kedua, validasi $request->validate() gagal otomatis melempar pengguna balik ke form beserta errornya. Kerja yang biasanya saya tulis manual bertahun lalu di PHP native, sekarang dua baris.

Dependency injection: minta, jangan dicari sendiri

Istilah ini dulu terdengar menakutkan, padahal idenya sederhana. Alih-alih sebuah class membuat sendiri kebutuhannya (misalnya new Request() di dalam method), kita cukup memintanya lewat parameter, dan container Laravel yang menyediakan. Contohnya type-hint Request $request di atas — saya tidak pernah membuat objek itu, Laravel yang menyuntikkannya. Ini analogi yang saya pegang: di restoran kita tidak masuk ke dapur lalu memasak sendiri; kita cukup memesan lewat pelayan. Container itulah sang pelayan.

// Constructor injection: kebutuhan disuntik sekali di konstruktor
public function __construct(protected LoggerInterface $logger)
{
    //
}

public function index(): View
{
    $this->logger->info('Halaman tugas dibuka');

    return view('tasks.index', ['tasks' => Task::all()]);
}

Manfaat terbesarnya ada di testing: karena dependensi disuntik, saya bisa menggantinya dengan versi palsu (mock) saat uji coba. Catatan untuk diri sendiri: jangan sampai memanggil helper seperti app(LoggerInterface::class) di tengah method hanya karena lebih singkat — itu menyalahi keuntungan DI-nya sendiri.

Tujuh method resource controller secara rinci

Saat pertama kali melihat tujuh method sekaligus, saya sempat bingung: method mana yang menerima data, mana yang hanya tampilan. Setelah saya catat dalam bentuk tabel, semuanya jadi jelas:

MethodHTTP VerbURIFungsi
index()GET/tasksDaftar semua data
create()GET/tasks/createTampilkan form tambah
store()POST/tasksSimpan data baru
show($id)GET/tasks/{task}Detail satu data
edit($id)GET/tasks/{task}/editTampilkan form ubah
update($id)PUT/PATCH/tasks/{task}Simpan perubahan
destroy($id)DELETE/tasks/{task}Hapus data

Pola yang saya petik: method dengan kata create dan edit hanya menampilkan form (tidak mengubah data), sedangkan store dan update yang benar-benar menulis ke database. Pemisahan ini rapi dan konsisten di seluruh proyek Laravel.

Form Request Validation: validasi yang terpisah dari controller

Di contoh sebelumnya validasi ditulis langsung di dalam method controller. Fungsi, tapi kalau aturannya rumit — misalnya ada dua puluh field — controller jadi panjang dan sulit dibaca. Laravel menyediakan FormRequest sebagai solusi: kelas validasi yang hidup sendiri, bisa dipakai ulang, dan di-test secara terpisah.

php artisan make:request StoreTaskRequest
namespace App\\Http\\Requests;

use Illuminate\\Foundation\\Http\\FormRequest;

class StoreTaskRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // sesuaikan dengan izin user
    }

    public function rules(): array
    {
        return [
            'title'       => ['required', 'max:255'],
            'description' => ['nullable', 'string'],
            'due_date'    => ['required', 'date', 'after:today'],
        ];
    }
}

Kemudian di controller cukup ganti type-hint Request menjadi StoreTaskRequest:

public function store(StoreTaskRequest $request): RedirectResponse
{
    $validated = $request->validated();
    Task::create($validated);

    return redirect()->route('tasks.index');
}

Yang saya pelajari: method validated() hanya mengembalikan data yang lolos rules, sehingga tidak perlu khawatir field ekstra masuk ke database. Membuat saya berpikir bahwa validasi yang bersih dimulai dari memisahkannya dari logika bisnis.

Constructor injection vs method injection

Ada dua cara dependency injection bekerja di controller Laravel, dan perbedaannya ternyata sederhana:

Constructor injection — dependensi disuntik sekali saat controller diinisialisasi. Cocok untuk service yang dibutuhkan di hampir semua method, seperti logger atau repository:

class TaskController extends Controller
{
    public function __construct(
        protected LoggerInterface $logger,
        protected TaskRepository  $repository,
    ) {
        //
    }

    public function index(): View
    {
        $this->logger->info('Membuka daftar tugas');
        return view('tasks.index', [
            'tasks' => $this->repository->all(),
        ]);
    }
}

Method injection — dependensi hanya disuntik di satu method tertentu. Berguna kalau hanya sebagian kecil method yang membutuhkan service itu:

use Illuminate\\Http\\UploadedFile;

class TaskController extends Controller
{
    public function store(
        StoreTaskRequest $request,
        UploadedFile $attachment,
    ): RedirectResponse {
        $path = $attachment->store('attachments');
        Task::create([...$request->validated(), 'file_path' => $path]);

        return redirect()->route('tasks.index');
    }
}

Prinsip yang saya pegang: kalau dependensi dipakai oleh banyak method, constructor injection. Kalau hanya satu method, method injection lebih ringkas. Keduanya sama-sama ditangani oleh service container — tidak ada yang lebih “benar”, hanya lebih cocok untuk konteks tertentu.

Single Action Controller (Invokable)

Tidak semua controller perlu tujuh method. Kadang saya hanya butuh satu halaman khusus — misalnya halaman dashboard atau proses export. Laravel menyediakan single action controller yang hanya punya satu method __invoke:

namespace App\\Http\\Controllers;

class ExportController extends Controller
{
    public function __invoke(): StreamedResponse
    {
        $tasks = Task::all();

        return response()->streamDownload(function () use ($tasks) {
            echo 'ID,Title,DueDate' . PHP_EOL;
            foreach ($tasks as $task) {
                echo "{$task->id},{$task->title},{$task->due_date}" . PHP_EOL;
            }
        }, 'tasks-export.csv');
    }
}

Route-nya pun ringkas:

use App\\Http\\Controllers\\ExportController;

Route::get('/tasks/export', ExportController::class);

Kapan pakai? Kalau logikanya linear — satu input, satu output, tanpa variasi method — invokable controller lebih jelas daripada memaksakan tujuh method yang tidak terpakai.

Konvensi penamaan controller dan PSR-4 autoloading

Salah satu kebiasaan baik yang saya pelajari: ikuti konvensi penamaan bawaan Laravel. Class TaskController berada di namespace App\Http\Controllers dan file-nya ada di app/Http/Controllers/TaskController.php. Ini bukan kebetulan — Laravel mengikuti standar PSR-4 autoloading, artinya namespace dan path file harus cocok persis.

Konvensi penamaan yang saya catat:

  • Nama class: TaskController — singular noun + Controller suffix.
  • Method: index, create, store, show, edit, update, destroy — urutan dan nama sudah dibakukan.
  • Resource route: Route::resource('tasks', TaskController::class) — nama route collection mengikuti nama resource.

Mengikuti konvensi ini bukan soal estetika semata. Ketika seluruh tim pakai pola yang sama, saya bisa membuka controller proyek lain dan langsung tahu di mana mencari method tertentu tanpa perlu membaca dokumentasi tambahan.

Catatan pribadi: momen paham kenapa DI penting

Saya ingat waktu pertama kali membuat controller tanpa dependency injection — setiap method saya menulis $db = new Database() atau $mailer = new Mailer(). Kode berjalan, tapi begitu saya ingin mengganti implementasi database dari MySQL ke PostgreSQL, saya harus mengedit belasan file satu per satu. Begitu saya mulai memahami prinsip “minta, jangan buat sendiri” — saya cukup mengganti binding di service container sekali, dan seluruh aplikasi langsung menggunakan implementasi baru. Itulah momen yang membuat saya berpikir: dependency injection bukan sekadar pola teknis, tapi investasi kemudahan di masa depan.

Kesimpulan

  • Controller = rumah bagi logika aplikasi; closure di route hanya untuk hal-hal sekecil mungkin.
  • php artisan make:controller Nama --resource menghasilkan tujuh method CRUD dengan konvensi baku.
  • Route::resource hemat banyak baris dan urutan routenya sudah aman.
  • Type-hint parameter (Request, Model) = dependency injection + route model binding bekerja diam-diam untuk kita.

Berikutnya episode lima saya akan masuk ke Blade templating yang lebih serius: layout master-child dan komponen. Sampai jumpa di catatan belajar berikutnya.


Sumber

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *

Situs ini menggunakan Akismet untuk mengurangi spam. Pelajari bagaimana data komentar Anda diproses