Menguasai Event-Driven Decoupling di Laravel
Controller Laravel Anda sering kali menjadi tempat penampungan logika bisnis.
Anda memulai dengan alur registrasi yang sederhana. Tak lama kemudian, Anda menambahkan notifikasi email, peringatan Slack, log audit, dan panggilan API ke dalam satu metode tunggal. Ini menciptakan fat controller.
Fat controller membuat kode Anda rapuh. Mereka sulit untuk diuji. Mereka melanggar Single Responsibility Principle.
Anda tidak memerlukan alat kompleks seperti RabbitMQ untuk memperbaiki ini. Laravel memiliki sistem event bawaan yang dapat memenuhi sebagian besar kebutuhan.
Masalah dengan tight coupling: Jika API newsletter lambat, registrasi pengguna Anda ikut melambat. Jika layanan email gagal, seluruh permintaan (request) akan gagal.
Solusinya: Event-Driven Architecture.
Event bertindak sebagai lapisan tengah. Controller Anda mengumumkan sebuah aksi. Listener bereaksi terhadap aksi tersebut secara independen.
Controller yang ramping terlihat seperti ini:
public function register(RegisterRequest $request)
{
$user = User::create($request->validated());
UserRegistered::dispatch($user);
return response()->json(['message' => 'Success'], 201);
}
Controller sekarang hanya menangani persistensi data. Ia tidak peduli dengan side effects.
Anda mendapatkan tiga manfaat utama:
- Performa: Pengguna mendapatkan respons secara instan. Tugas-tugas berat berjalan di latar belakang menggunakan interface
ShouldQueue. - Resiliensi: Jika sebuah layanan sedang down, listener dapat mencoba kembali tugas tersebut tanpa merusak aplikasi utama.
- Ekstensibilitas: Anda dapat menambahkan fitur baru, seperti push notification, dengan menambahkan listener baru. Anda tidak perlu menyentuh controller aslinya.
Praktik terbaik yang perlu diikuti:
- Fokus pada side effects: Gunakan event untuk pemrosesan lanjutan (post-processing). Jangan gunakan event untuk logika inti yang harus terjadi secara instan.
- Gunakan nama yang deskriptif: Gunakan nama dalam bentuk lampau (past-tense) seperti
OrderPlacedatauUserRegistered. Ini menunjukkan bahwa aksi tersebut telah terjadi. - Hindari over-abstraction: Jika sebuah potongan kode sederhana dan hanya digunakan di satu tempat, pemanggilan fungsi lebih baik daripada menggunakan event.
Gunakan Eloquent Observers untuk perubahan database. Gunakan Events untuk aksi bisnis.
Refactoring ke event adalah tentang daya tahan (durability). Ini membuat kode Anda lebih mudah di-debug dan lebih cepat untuk diuji.
Pilih satu side effect yang mengganggu di controller Anda dan pindahkan ke listener hari ini.
Coba contoh sandbox ini: https://onlinephp.io/c/1f7b2
Melampaui Fat Controllers: Menguasai Event-Driven Decoupling di Laravel
Pernahkah Anda membangun aplikasi Laravel di mana controller Anda tampak melakukan segalanya? Dari validasi data, menyimpan ke database, mengirim email, hingga memicu notifikasi pihak ketiga—semuanya menumpuk dalam satu metode tunggal.
Meskipun cara ini mungkin bekerja untuk proyek kecil, seiring berkembangnya aplikasi, pendekatan ini akan menjadi mimpi buruk pemeliharaan. Inilah yang kita sebut sebagai masalah "Fat Controller" (Controller yang terlalu gemuk).
Dalam artikel ini, kita akan membahas mengapa Fat Controller berbahaya dan bagaimana Anda dapat menggunakan Event-Driven Decoupling untuk menciptakan kode yang lebih bersih, modular, dan mudah dikelola.
Masalahnya: Controller yang Membengkak
Mari kita lihat contoh controller tipikal yang menangani pendaftaran pengguna baru:
namespace App\Http\Controllers;
use App\Models\User;
use App\Mail\WelcomeMail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class UserController extends Controller
{
public function store(Request $request)
{
// 1. Validasi data
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
// 2. Membuat pengguna
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => bcrypt($validated['password']),
]);
// 3. Mengirim email selamat datang
Mail::to($user->email)->send(new WelcomeMail($user));
// 4. Mencatat log aktivitas
Log::info("Pengguna baru terdaftar: {$user->email}");
// 5. Memberi tahu admin melalui API pihak ketiga
// (Misalnya, mengirim webhook ke Slack)
// Http::post('https://hooks.slack.com/...', ['text' => 'User registered!']);
return response()->json(['message' => 'User created successfully'], 201);
}
}
Mengapa ini buruk?
- Pelanggaran Single Responsibility Principle (SRP): Controller ini bertanggung jawab atas terlalu banyak hal. Tugas utamanya seharusnya hanya menerima permintaan dan mengembalikan respons, bukan mengelola logika bisnis yang kompleks.
- Sulit Diuji (Hard to Test): Untuk menguji metode
store, Anda harus mensimulasikan pengiriman email, logging, dan bahkan panggilan API pihak ketiga. - Sulit Diubah (Rigid): Jika Anda ingin mengubah cara email dikirim atau menambahkan tugas baru (seperti memberikan bonus poin kepada pengguna baru), Anda harus kembali ke controller ini dan menambahkannya, yang meningkatkan risiko merusak logika pendaftaran yang sudah ada.
Solusinya: Decoupling dengan Event dan Listener
Decoupling adalah proses mengurangi ketergantungan antara berbagai bagian dari sebuah aplikasi. Dalam konteks Laravel, kita dapat menggunakan Events dan Listeners untuk memisahkan logika utama (pendaftaran pengguna) dari efek sampingnya (mengirim email, logging, dll.).
Dengan pendekatan ini, controller hanya akan melakukan satu hal: memicu (dispatch) sebuah event.
Implementasi Langkah demi Langkah
Mari kita ubah controller di atas menggunakan arsitektur berbasis event.
Langkah 1: Buat Event
Pertama, buatlah event yang merepresentasikan kejadian tersebut menggunakan Artisan:
php artisan make:event UserRegistered
Sekarang, buka file app/Events/UserRegistered.php dan tambahkan data pengguna yang diperlukan:
namespace App\Events;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered
{
use Dispatchable, SerializesModels;
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
Langkah 2: Buat Listener
Sekarang, kita buat listener untuk menangani tugas-tugas spesifik. Kita akan membuat satu listener untuk setiap tugas agar tetap modular.
Listener untuk Email Selamat Datang:
php artisan make:listener SendWelcomeEmail --event=UserRegistered
Listener untuk Logging:
php artisan make:listener LogUserRegistration --event=UserRegistered
Isi file app/Listeners/SendWelcomeEmail.php:
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail
{
public function handle(UserRegistered $event)
{
Mail::to($event->user->email)->send(new WelcomeMail($event->user));
}
}
Langkah 3: Daftarkan Event dan Listener
Jika Anda menggunakan Laravel versi lama, Anda perlu mendaftarkannya di EventServiceProvider. Namun, pada Laravel versi terbaru, Anda dapat menggunakan Event Discovery atau mendaftarkannya secara manual di app/Providers/EventServiceProvider.php:
protected $listen = [
UserRegistered::class => [
SendWelcomeEmail::class,
LogUserRegistration::class,
// Anda bisa dengan mudah menambah listener baru di sini!
],
];
Langkah 4: Refactor Controller
Sekarang, mari kita lihat betapa bersihnya UserController kita sekarang:
namespace App\Http\Controllers;
use App\Models\User;
use App\Events\UserRegistered;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => bcrypt($validated['password']),
]);
// Hanya memicu event!
event(new UserRegistered($user));
return response()->json(['message' => 'User created successfully'], 201);
}
}
Keuntungan Arsitektur Berbasis Event
- Maintainability (Kemudahan Pemeliharaan): Setiap kelas memiliki satu tanggung jawab. Jika ada masalah dengan pengiriman email, Anda tahu persis di mana harus mencarinya (
SendWelcomeEmail), tanpa takut merusak logika pembuatan user. - Scalability (Skalabilitas): Anda dapat dengan mudah menambahkan fitur baru. Ingin memberikan diskon kepada pengguna baru? Cukup buat
GiveNewUserDiscountlistener dan tambahkan ke daftar event. Anda tidak perlu menyentuh controller sama sekali. - Testability (Kemudahan Pengujian): Anda dapat menguji pembuatan user secara terpisah dari pengiriman email. Anda juga dapat menggunakan
Event::fake()dalam pengujian untuk memastikan event dipicu tanpa benar-benar mengirim email sungguhan. - Asynchronous Processing (Pemrosesan Asinkron): Ini adalah keuntungan terbesar. Dengan mengimplementasikan interface
ShouldQueuepada listener Anda, Laravel akan menjalankan tugas tersebut di latar belakang (background job), sehingga respons API Anda menjadi jauh lebih cepat bagi pengguna.
class SendWelcomeEmail implements ShouldQueue
{
// Listener ini sekarang akan berjalan di background!
}
Kesimpulan
Menghentikan kebiasaan membuat "Fat Controllers" adalah langkah besar menuju penulisan kode yang profesional. Dengan mengadopsi Event-Driven Decoupling, Anda mengubah aplikasi Anda dari tumpukan kode yang kaku menjadi sistem yang modular, fleksibel, dan siap untuk berkembang.
Mulailah dengan mengidentifikasi tugas-tugas "sampingan" di controller Anda dan pindahkan mereka ke dalam Event dan Listener. Tubuh aplikasi Anda akan berterima kasih!