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.

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 - hapusYang 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:
| Method | HTTP Verb | URI | Fungsi |
|---|---|---|---|
| index() | GET | /tasks | Daftar semua data |
| create() | GET | /tasks/create | Tampilkan form tambah |
| store() | POST | /tasks | Simpan data baru |
| show($id) | GET | /tasks/{task} | Detail satu data |
| edit($id) | GET | /tasks/{task}/edit | Tampilkan 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 +Controllersuffix. - 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 --resourcemenghasilkan tujuh method CRUD dengan konvensi baku.Route::resourcehemat 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.
Komentar Terbaru