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);
make:eventmembuatapp/Events/BookBorrowed.php.make:listener ... --event=BookBorrowedmembuatapp/Listeners/SendLoanReceipt.phpyang sudah punyahandle(BookBorrowed $event).public function __construct(public Loan $loan) {}: event cuma wadah data. Datanya masuk lewat constructor dan disimpan sebagai property$loan.handle(BookBorrowed $event): typeBookBorroweddi sini yang memberi tahu Laravel listener ini milik event mana. Laravel memindai folderapp/Listenersdan mendaftarkannya sendiri (event discovery).php artisan event:listmenampilkan pasangannya.$event->loan: listener membaca data dari event yang dioper kehandle().BookBorrowed::dispatch($loan)memancarkan event. Argumennya diteruskan ke constructor. Sama artinya denganevent(new BookBorrowed($loan)).
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));
}
}
- Saat
dispatch, listener ini gak dijalankan. Laravel cuma menyimpannya sebagai job di antrean, yaitu tabeljobs(project baru memakaiQUEUE_CONNECTION=databasedi.env). Itu makan sekitar 4 ms. - Proses terpisah,
php artisan queue:work, mengambil job itu lalu menjalankanhandle(). Detailnya di lesson Queue & Job. - Respons sekarang 6 + 5 + 4 + 3 = 18 ms. Itu jawaban tebakannya. Email-nya tetap terkirim, cuma sedikit lebih lambat dari halamannya.
| 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:
LogLoanActivitygak pernah jalan, karena listener sesudah yang gagal dilewati.- Request jawab
500 | Server Error. - Yang sudah jalan gak dibatalkan: peminjaman #12 sudah tercatat dan stok sudah berkurang. Kalau Budi mencoba lagi, dia meminjam dua kali.
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
handle()tanpa type event. TanpaBookBorrowed $event, Laravel gak tahu listener ini milik event mana, dan listener-nya gak pernah jalan. Cek denganphp artisan event:list.- Sudah
ShouldQueuetapi tetap lambat, atau email gak pernah datang.QUEUE_CONNECTION=syncmenjalankan job saat itu juga. Kalaudatabase, harus ada worker yang jalan, kalau gak job menumpuk di tabeljobs. - Satu listener butuh hasil listener lain. Listener sync memang jalan berurutan, tapi tiap listener sebaiknya berdiri sendiri. Kalau urutannya penting, kerjakan berurutan di satu tempat.
- Di latihan, hasilnya gak dicetak.
handle()yang mengembalikan teks belum menampilkan apa-apa. Panggil dan cetak:echo $listener->handle($event);, dan baca datanya lewat$event->....
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.