Event & Listener — Laravel

Events memisahkan "apa yang terjadi" dari "apa yang harus dilakukan" — loose coupling. Contoh: Saat user register, kita ingin: Kirim email welcome Buat record…

Event adalah pengumuman bahwa sesuatu baru saja terjadi, misalnya "buku dipinjam". Listener adalah kode yang bereaksi pada pengumuman itu. Bayangin bel istirahat sekolah: bel bunyi sekali, lalu kantin buka, satpam membuka gerbang, dan murid keluar kelas. Bel gak tahu siapa saja yang bereaksi, dan tiap orang tahu sendiri tugasnya. Event itu belnya, listener itu orang-orang yang bereaksi.

🧪 Tebakan cepat: di app Perpustakaan, saat buku dipinjam ada tiga listener: kurangi stok (5 ms), kirim email struk (1.200 ms), dan catat log (3 ms). Kalau listener email dijadikan ShouldQueue, browser menunggu sekitar berapa lama: 1.214 ms, 18 ms, atau 0 ms? Tebak dulu, jawabannya ada di bagian "Sync atau ShouldQueue".

Masalahnya: controller yang mengurus semuanya

Di lesson Controller, LoanController@store mencatat peminjaman. Lama-lama tugasnya bertambah: kurangi stok, kirim email struk ke anggota, catat log untuk pustakawan. Kalau semuanya ditulis di controller, tiap tugas baru berarti mengubah controller lagi, dan anggota ikut menunggu email-nya terkirim sebelum halaman terbuka.

Dengan event, controller cukup mengumumkan "buku dipinjam". Siapa yang bereaksi diurus di class lain.

Event dan listener pertamamu

Buat keduanya dengan Artisan:

php artisan make:event BookBorrowed
php artisan make:listener SendLoanReceipt --event=BookBorrowed
<?php
// app/Events/BookBorrowed.php
namespace App\Events;

use App\Models\Loan;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class BookBorrowed
{
    use Dispatchable, SerializesModels;

    public function __construct(public Loan $loan) {}
}
<?php
// app/Listeners/SendLoanReceipt.php
namespace App\Listeners;

use App\Events\BookBorrowed;
use App\Mail\LoanReceipt;
use Illuminate\Support\Facades\Mail;

class SendLoanReceipt
{
    public function handle(BookBorrowed $event): void
    {
        Mail::to($event->loan->user)->send(new LoanReceipt($event->loan));
    }
}
// app/Http/Controllers/LoanController.php, di dalam store()
$loan = Loan::create(['user_id' => $request->user()->id, 'book_id' => $book->id]);

BookBorrowed::dispatch($loan); // umumkan: buku dipinjam

return response()->json(['id' => $loan->id], 201);

Satu event, tiga listener

Tambah dua listener lagi, UpdateBookStock dan LogLoanActivity, dengan cara yang sama. Controller gak berubah sama sekali. Begini yang terjadi saat Budi meminjam "Laskar Pelangi" (stok 3), kalau semua listener masih biasa (sync, dijalankan saat itu juga):

Urutan Yang jalan Waktu Hasilnya
1 Loan::create(...) di controller 6 ms peminjaman #12 tercatat
2 UpdateBookStock 5 ms stok 3 jadi 2
3 SendLoanReceipt 1.200 ms email struk terkirim ke Budi
4 LogLoanActivity 3 ms satu baris di log
5 respons 201 total 1.214 ms halaman Budi baru terbuka

Listener sync jalan satu per satu, di request yang sama. Browser menunggu semuanya selesai, termasuk email yang lambat karena harus bicara dengan server email.

Sync atau ShouldQueue

Tugas yang lambat dan gak harus selesai sebelum respons bisa ditunda. Tambahkan implements ShouldQueue:

use Illuminate\Contracts\Queue\ShouldQueue;

class SendLoanReceipt implements ShouldQueue
{
    public int $tries = 3; // dicoba paling banyak 3 kali

    public function handle(BookBorrowed $event): void
    {
        Mail::to($event->loan->user)->send(new LoanReceipt($event->loan));
    }
}
Sync ShouldQueue
Kapan jalan saat dispatch, di request itu nanti, saat worker mengambilnya
Siapa yang menunggu browser gak ada
Kalau gagal request jawab 500 dicoba lagi sesuai $tries
Cocok untuk yang harus beres sebelum respons (stok) yang lambat atau memanggil server lain (email)

Kalau listener gagal

Misalnya server email mati dan SendLoanReceipt masih sync. Mail::send() melempar exception, jadi:

Kalau listener-nya ShouldQueue, request tetap 201. Job-nya gagal di worker, dicoba lagi sampai 3 kali, lalu masuk tabel failed_jobs.

Satu lagi: listener yang mengembalikan false dari handle() menghentikan listener berikutnya. Itu disengaja, bukan error.

⚠️ Jebakan yang sering ketemu

TL;DR: Event mengumumkan bahwa sesuatu terjadi, listener bereaksi. Buat dengan php artisan make:event BookBorrowed dan php artisan make:listener SendLoanReceipt --event=BookBorrowed. Event membawa data lewat constructor (public Loan $loan), listener membacanya di handle(BookBorrowed $event) lewat $event->loan, dan controller cukup memanggil BookBorrowed::dispatch($loan). Listener sync jalan berurutan di request yang sama dan ditunggu browser (1.214 ms). implements ShouldQueue menundanya ke worker, jadi respons 18 ms. Listener sync yang gagal membuat request 500 dan melewati listener sesudahnya, tanpa membatalkan yang sudah jalan.

Yang akan kamu pelajari