โ† Semua pembelajaran / Laravel Nol โ†’ Enterprise
Fase 3 ยท Arsitektur Laravel

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.

Sumber asli laravel.com Resmi Rangkuman ~6 menit baca

Intisari

  • Event = kelas data. Listener = kelas yang bereaksi. Keduanya ditemukan otomatis lewat tipe di handle().
  • Listener yang mengimplementasikan ShouldQueue berjalan 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

KebutuhanPakai
Efek samping yang boleh gagal sendiriEvent + listener antrean
Langkah wajib yang harus atomikTulis berurutan di dalam DB::transaction()
Satu pekerjaan berat, satu pemanggilJob langsung โ€” event menambah lapisan tanpa manfaat
Alur bertahap dengan syaratKelas action, atau job berantai (Bus::chain)
Komunikasi ke sistem lainEvent, 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.