Event & listener
Event membuat satu perbuatan bisa memicu banyak akibat tanpa saling mengenal. Ia sangat berguna, dan juga sangat mudah disalahgunakan sampai tidak ada yang tahu lagi apa yang terjadi saat pesanan dibuat.
Intisari
- Event = kelas data. Listener = kelas yang bereaksi. Keduanya ditemukan otomatis lewat tipe di
handle(). - Listener yang mengimplementasikan
ShouldQueueberjalan di antrean โ permintaan pengguna tidak menunggunya. Event::fake()membuat tes bisa memastikan event terpancar tanpa menjalankan akibatnya.- Model memancarkan event sendiri (
created,updated,deleted) โ kuat, tapi mudah jadi ajaib. - Aturan sehat: pakai event untuk efek samping, bukan untuk langkah yang wajib berhasil.
Bentuknya
// app/Events/PesananDibayar.php โ cuma pembawa data, tidak ada logika
class PesananDibayar
{
use Dispatchable, SerializesModels;
public function __construct(public readonly Pesanan $pesanan) {}
}
// app/Listeners/KirimStrukPembayaran.php
class KirimStrukPembayaran implements ShouldQueue
{
public function handle(PesananDibayar $event): void // โ tipe di sini yang mendaftarkannya
{
Mail::to($event->pesanan->user)->send(new Struk($event->pesanan));
}
}
// memancarkannya
PesananDibayar::dispatch($pesanan);
Tidak ada registrasi manual. Laravel memindai folder app/Listeners dan mencocokkan berdasarkan tipe
parameter handle(). Menambah akibat baru berarti menambah satu berkas โ kode yang memancarkan event
tidak disentuh sama sekali.
Kekuatan sesungguhnya: satu peristiwa, banyak akibat
PesananDibayar
โโโ KirimStrukPembayaran (antrean) โ email ke pembeli
โโโ KurangiStok (langsung) โ update tabel
โโโ BeriTahuGudang (antrean) โ webhook ke sistem gudang
โโโ CatatKeAnalitik (antrean) โ kirim ke layanan analitik
โโโ BerikanPoinLoyalitas (antrean) โ tambah poin
Dan di sinilah bahayanya. Enam bulan kemudian, seseorang bertanya "kenapa stok berkurang dua kali?" dan tidak ada satu berkas pun yang bisa dibaca untuk menjawabnya โ jawabannya tersebar di lima listener yang tidak saling tahu. Batas sehatnya: event untuk efek samping yang boleh gagal sendiri-sendiri; langkah yang wajib berhasil tetap ditulis berurutan di satu tempat yang bisa dibaca. Mengurangi stok termasuk yang wajib โ ia layak berada di dalam transaksi bersama pembuatan pesanan, bukan di listener.
Listener antrean
class KirimStrukPembayaran implements ShouldQueue
{
public $queue = 'email';
public $tries = 3;
public $backoff = [10, 60, 300]; // jeda antar percobaan, dalam detik
public function handle(PesananDibayar $e): void { /* ... */ }
public function failed(PesananDibayar $e, Throwable $error): void
{
Log::error('struk gagal terkirim', [
'pesanan' => $e->pesanan->id,
'sebab' => $error->getMessage(),
]);
}
}
SerializesModels menyimpan ID, bukan objeknya. Saat job dijalankan worker beberapa detik
kemudian, model diambil ulang dari database. Dua konsekuensinya: (1) kalau barisnya sudah dihapus, job gagal
dengan ModelNotFoundException; (2) nilainya adalah nilai saat job dijalankan, bukan saat
event dipancarkan. Kalau kamu butuh nilai lama, salin ke properti biasa di event-nya.
Event bawaan model
protected $dispatchesEvents = [
'created' => ProdukDibuat::class,
'deleted' => ProdukDihapus::class,
];
// Atau lewat observer โ satu kelas untuk semua event satu model
class ProdukObserver
{
public function created(Produk $p): void { /* ... */ }
public function updated(Produk $p): void { /* ... */ }
public function deleted(Produk $p): void { /* ... */ }
}
Event model tidak berjalan pada operasi massal. Produk::where(...)->update([...]) dan
DB::table(...)->delete() melewati Eloquent sepenuhnya โ tidak ada observer yang terpanggil.
Ini bug klasik: berkas terkait tidak ikut terhapus, cache tidak ikut dibersihkan, indeks pencarian jadi basi.
Kalau kebenarannya wajib, jangan gantungkan pada event model.
Menguji
// Memastikan event dipancarkan, tanpa menjalankan listener-nya
Event::fake([PesananDibayar::class]);
$this->post('/bayar/1');
Event::assertDispatched(PesananDibayar::class,
fn ($e) => $e->pesanan->id === 1);
Kapan bukan event
| Kebutuhan | Pakai |
|---|---|
| Efek samping yang boleh gagal sendiri | Event + listener antrean |
| Langkah wajib yang harus atomik | Tulis berurutan di dalam DB::transaction() |
| Satu pekerjaan berat, satu pemanggil | Job langsung โ event menambah lapisan tanpa manfaat |
| Alur bertahap dengan syarat | Kelas action, atau job berantai (Bus::chain) |
| Komunikasi ke sistem lain | Event, lalu listener yang mengirim pesan ke antrean/bus |
Latihan: buat event PesananDibayar dengan dua listener โ satu mengirim email (antrean),
satu mencatat log (langsung). Jalankan dengan QUEUE_CONNECTION=sync lalu dengan
database, dan amati perbedaan waktu respons yang dirasakan pengguna.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.