Kalau kamu pernah bingung mau taruh logika di mana — di Observer, di Event Listener, atau langsung di Model — kamu tidak sendirian. Ketiganya bisa menjalankan hal yang “mirip”. Tapi masing-masing punya tempat yang tepat, dan salah pilih bisa bikin kode yang susah dilacak.
Artikel ini bahas tuntas perbedaannya, kapan pakai yang mana, dan bagaimana Event bisa diangkat satu level lebih jauh: dikirim ke browser secara real-time lewat WebSocket.
🤔 Dua Jenis “Reaksi” yang Berbeda
Sebelum masuk ke kode, ada satu kalimat yang perlu dipahami dulu:
Observer itu reaktif ke data. Event itu reaktif ke makna.
Artinya: Observer peduli pada perubahan data di model. Event peduli pada sesuatu yang terjadi di alur bisnis aplikasi.
Kedengarannya mirip, tapi implikasinya sangat berbeda. Mari mulai dari Observer.
👁️ Observer — “Pengamat Model”
Analoginya
Bayangin Observer seperti satpam gudang yang selalu berjaga.
Setiap kali ada barang yang masuk, dipindahkan, atau diambil dari gudang — satpam itu otomatis bereaksi. Dia tidak peduli kenapa barang itu bergerak, siapa yang memindahkan, atau apa konteks bisnisnya. Tugasnya satu: kalau model berubah, jalankan ini.
Observer di Laravel persis seperti itu — dia menempel ke lifecycle model (Eloquent) dan otomatis bereaksi setiap kali model melewati fase tertentu.
Aturan Observer
- Satu Observer = satu Model.
- Hanya bisa merespons hook lifecycle bawaan Eloquent:
creating,created,updating,updated,deleting,deleted,saving,saved,restoring,restored. - Tidak perlu di-trigger manual — dia otomatis jalan setiap kali model melewati event tersebut.
- Didaftarkan di
AppServiceProvider::boot().
Kodenya
php artisan make:observer UserObserver --model=User
📁 app/Observers/UserObserver.php
<?php
namespace App\Observers;
use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
class UserObserver
{
// Jalan SEBELUM user disimpan ke DB untuk pertama kali
public function creating(User $user): void
{
// Auto-generate UUID — ini urusan data, bukan bisnis
$user->uuid = Str::uuid();
}
// Jalan SETELAH user dihapus dari DB
public function deleted(User $user): void
{
// Bersihkan file yang tidak lagi dibutuhkan
Storage::delete($user->avatar);
}
}
Daftarkan di AppServiceProvider:
public function boot(): void
{
User::observe(UserObserver::class);
}
📌 Perhatikan — logika di dalam Observer tidak ada kaitannya dengan bisnis. Str::uuid() adalah urusan integritas data. Storage::delete() adalah urusan kebersihan data. Tidak ada logika “kirim email selamat datang” atau “notifikasi admin” di sini — itu bukan tugas Observer.
Kapan Pakai Observer
Pakai Observer untuk efek samping yang melekat ke model — hal-hal yang harus terjadi setiap kali model berubah, terlepas dari konteks bisnis apapun yang memicunya. Misalnya: auto-generate slug, auto-fill created_by, hapus file terkait saat model dihapus, atau invalidate cache saat data diupdate.
📢 Event & Listener — “Pengumuman dan Pendengar”
Analoginya
Bayangin Event seperti pengeras suara di kantor yang mengumumkan sesuatu.
“Perhatian: ada pelanggan baru yang baru saja mendaftar!”
Pengeras suara tidak peduli siapa yang mendengar, dan tidak peduli apa yang mereka lakukan setelah dengar. Tugasnya cuma mengumumkan.
Yang peduli adalah para pendengar — tim marketing langsung kirim email sambutan, tim data langsung catat ke analytics, tim sales langsung assign account manager. Masing-masing bereaksi sendiri-sendiri, tanpa saling tahu.
Event & Listener di Laravel persis seperti itu.
- Event = pengumuman bahwa sesuatu telah terjadi
- Listener = pihak yang bereaksi atas pengumuman itu — dan boleh ada banyak
Aturan Event & Listener
- Event adalah class biasa yang membawa data konteks kejadian.
- Satu Event bisa punya banyak Listener — masing-masing bereaksi sendiri tanpa saling ketergantungan.
- Listener bisa implements
ShouldQueue— artinya reaksinya bisa dijalankan di background. - Event di-trigger manual dengan
event(new NamaEvent(...)). - Tidak terikat lifecycle model — bisa ditrigger dari mana saja.
Kodenya
php artisan make:event UserRegistered
php artisan make:listener SendWelcomeEmail --event=UserRegistered
php artisan make:listener NotifyAdminSlack --event=UserRegistered
📁 app/Events/UserRegistered.php
Event adalah “amplop” yang membawa informasi tentang apa yang terjadi.
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Queue\SerializesModels;
class UserRegistered
{
use SerializesModels;
public function __construct(
public User $user
) {}
}
📁 app/Listeners/SendWelcomeEmail.php
Listener pertama — kirim email sambutan.
<?php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Mail\WelcomeMail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
// ShouldQueue = reaksi ini dijalankan di background, tidak blokir request
class SendWelcomeEmail implements ShouldQueue
{
public function handle(UserRegistered $event): void
{
Mail::to($event->user->email)->send(new WelcomeMail($event->user));
}
}
📁 app/Listeners/NotifyAdminSlack.php
Listener kedua — notifikasi ke Slack. Event yang sama, reaksi yang berbeda.
<?php
namespace App\Listeners;
use App\Events\UserRegistered;
use Illuminate\Contracts\Queue\ShouldQueue;
class NotifyAdminSlack implements ShouldQueue
{
public function handle(UserRegistered $event): void
{
// Kirim notifikasi ke channel Slack admin
// Logika ini sepenuhnya terpisah dari listener lainnya
}
}
Daftarkan di EventServiceProvider (atau AppServiceProvider di Laravel 11+):
protected $listen = [
UserRegistered::class => [
SendWelcomeEmail::class,
NotifyAdminSlack::class,
],
];
Trigger dari Controller atau Service:
public function register(RegisterRequest $request)
{
$user = User::create($request->validated());
// Cukup umumkan — listener yang akan urus sisanya
event(new UserRegistered($user));
return response()->json(['message' => 'Registrasi berhasil'], 201);
}
📌 Perhatikan bagaimana Controller tidak tahu dan tidak peduli bahwa ada email yang dikirim dan ada Slack yang dinotifikasi. Controller cukup bilang “user sudah terdaftar”. Siapa yang bereaksi dan bagaimana caranya — itu urusan listener masing-masing.
Kapan Pakai Event & Listener
Pakai Event & Listener untuk logika bisnis yang punya makna — kejadian yang memang berarti sesuatu dalam konteks aplikasimu. Misalnya: OrderPlaced, PaymentFailed, UserPromotedToAdmin, SubscriptionExpired. Kejadian-kejadian ini bukan sekadar “model berubah” — mereka adalah momen penting yang mungkin perlu direspons oleh banyak bagian sistem.
🗺️ Gambaran Besar — Posisi Tiap Komponen
app/
├── Events/
│ ├── UserRegistered.php ← "Pengumuman" — apa yang terjadi
│ └── OrderPaid.php
│
├── Listeners/
│ ├── SendWelcomeEmail.php ← "Pendengar" — bereaksi atas pengumuman
│ └── NotifyAdminSlack.php
│
├── Observers/
│ └── UserObserver.php ← "Satpam" — reaktif ke perubahan model
Cara memilihnya:
Ada perubahan data di model? (creating, updated, deleted)
↓
→ Gunakan Observer
Ada kejadian bisnis yang bermakna? (user daftar, order dibayar)
↓
→ Gunakan Event & Listener
Logic itu harus menjadi bagian dari model itu sendiri?
↓
→ Gunakan Trait
📻 Broadcast Event — Bawa Real-Time ke Frontend
Sampai di sini, semua yang dibahas adalah komunikasi internal — antar komponen di dalam server. Event di-fire, Listener bereaksi, selesai.
Tapi ada skenario di mana reaksi tidak cukup di server. User yang sedang membuka browser perlu tahu seketika — tanpa refresh. Order masuk, notifikasi langsung muncul. Pembayaran berhasil, halaman langsung update.
Di sinilah Broadcast Event masuk.
Bagaimana Cara Kerjanya
Laravel tidak mengirim WebSocket secara langsung. Yang Laravel lakukan adalah mempublish event ke server WebSocket (Pusher, Soketi, atau Ably). Server WebSocket itu yang kemudian mendorong data ke semua client yang sedang terhubung.
User bayar → Controller → OrderPaid event di-fire
↓
Laravel publish ke broadcaster (Soketi / Pusher)
↓
Broadcaster push via WebSocket ke semua client
↓
Laravel Echo (JS) menerima → UI update tanpa reload
Empat Komponen Broadcast
| Komponen | Peran |
|---|---|
| Event | Apa yang terjadi di server |
| Broadcast Channel | Siapa yang boleh mendengar |
| Broadcaster | Server WebSocket (Pusher / Soketi / Ably) |
| Laravel Echo | Listener di sisi JavaScript / browser |
Implementasi Step by Step
Step 1 — Setup Broadcaster
Pilihan umum: Pusher (cloud, berbayar per koneksi) atau Soketi (self-host, gratis, kompatibel Pusher). Untuk development, Soketi adalah pilihan paling praktis.
# .env
BROADCAST_CONNECTION=pusher
PUSHER_APP_ID=local
PUSHER_APP_KEY=local
PUSHER_APP_SECRET=local
PUSHER_HOST=127.0.0.1
PUSHER_PORT=6001
PUSHER_SCHEME=http
Step 2 — Buat Event yang Bisa Di-Broadcast
php artisan make:event OrderPaid
📁 app/Events/OrderPaid.php
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class OrderPaid implements ShouldBroadcast
{
use SerializesModels;
public function __construct(
public Order $order
) {}
// Tentukan di channel mana event ini di-broadcast
public function broadcastOn(): Channel
{
// Private channel — hanya pemilik order yang boleh dengar
return new PrivateChannel('orders.' . $this->order->user_id);
}
}
📌 implements ShouldBroadcast — ini yang membedakan event biasa dari broadcast event. Tanpa interface ini, event hanya berjalan internal di server dan tidak akan pernah sampai ke browser.
Step 3 — Otorisasi Channel
📁 routes/channels.php
use Illuminate\Support\Facades\Broadcast;
// Public channel — siapapun boleh dengar
Broadcast::channel('notifications', function ($user) {
return true;
});
// Private channel — hanya user yang punya order ini
Broadcast::channel('orders.{userId}', function ($user, $userId) {
return $user->id === (int) $userId;
});
Step 4 — Trigger Event
// Dari Controller, Service, atau Job — sama saja
public function pay(Order $order)
{
$order->update(['status' => 'paid']);
// Fire event — Laravel otomatis broadcast ke Soketi
event(new OrderPaid($order));
return response()->json(['message' => 'Pembayaran berhasil']);
}
Step 5 — Setup Laravel Echo di Frontend
npm install laravel-echo pusher-js
📁 resources/js/bootstrap.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'local',
wsHost: window.location.hostname,
wsPort: 6001,
forceTLS: false,
disableStats: true,
});
Step 6 — Listen di Frontend
// Di komponen Vue, React, atau script biasa
const userId = {{ auth()->id() }};
Echo.private('orders.' + userId)
.listen('OrderPaid', (event) => {
console.log('Order dibayar:', event.order);
// Update UI tanpa reload
showNotification('Pembayaran berhasil! Order #' + event.order.id);
updateOrderStatus(event.order.id, 'paid');
});
📌 Nama event yang dipakai di .listen() adalah nama class PHP-nya (OrderPaid), bukan string manual. Laravel otomatis mengonversinya.
📡 Public vs Private vs Presence Channel
Tidak semua data boleh diterima siapa saja. Laravel punya tiga jenis channel dengan tingkat akses berbeda.
Public Channel — tidak butuh autentikasi. Cocok untuk data yang memang publik: harga saham live, jumlah pengunjung online, notifikasi global.
return new Channel('announcements');
Echo.channel('announcements').listen('NewAnnouncement', (e) => { ... });
Private Channel — hanya user yang sudah login dan lolos otorisasi yang bisa masuk. Cocok untuk notifikasi personal, update order milik sendiri, atau chat privat.
return new PrivateChannel('orders.' . $this->order->user_id);
Echo.private('orders.' + userId).listen('OrderPaid', (e) => { ... });
Presence Channel — seperti Private Channel, tapi semua member bisa saling tahu siapa saja yang sedang terhubung. Cocok untuk fitur “siapa yang sedang online”, indikator “sedang mengetik”, atau collaborative editing.
return new PresenceChannel('room.' . $this->roomId);
Echo.join('room.' + roomId)
.here((users) => { /* daftar user yang online */ })
.joining((user) => { /* user baru masuk */ })
.leaving((user) => { /* user keluar */ });
⚠️ Kesalahan yang Sering Terjadi
Observer untuk logika bisnis. Observer bereaksi ke perubahan data, bukan ke kejadian bisnis. Kalau kamu taruh “kirim email selamat datang” di Observer created, semua kode yang membuat user — termasuk seeder, factory testing, atau import data — juga akan trigger email itu. Bukan yang kamu mau.
Heavy logic di dalam Event class. Event adalah sinyal, bukan processor. Event class cukup membawa data konteks. Logikanya ada di Listener, bukan di Event.
Pakai Observer untuk real-time. Observer tidak bisa di-broadcast. Kalau kamu butuh perubahan model dikirim ke browser secara real-time, fire Event dari Observer — lalu broadcast Event itu.
Lupa ShouldBroadcast. Event tanpa implements ShouldBroadcast hanya berjalan internal. Tidak ada yang sampai ke browser, tidak ada error — hanya diam.
📊 Perbandingan Singkat
| Observer | Event & Listener | Broadcast Event | |
|---|---|---|---|
| Trigger | Otomatis (lifecycle model) | Manual (event(...)) | Manual (event(...)) |
| Terikat model | ✅ Ya | ❌ Tidak harus | ❌ Tidak harus |
| Bisa banyak reaksi | ❌ Satu Observer | ✅ Banyak Listener | ✅ Banyak Listener |
| Bisa queue (async) | ❌ | ✅ ShouldQueue | ✅ ShouldQueue |
| Sampai ke browser | ❌ | ❌ | ✅ Via WebSocket |
| Cocok untuk | Data hygiene | Business flow | Real-time UI |
💡 Ingat!
Satu kalimat yang merangkum semuanya:
- Observer → data berubah, ini yang harus beres
- Event Listener → sesuatu terjadi, ini yang harus dilakukan
- Broadcast Event → sesuatu terjadi, browser perlu tahu sekarang
Dan satu prinsip yang paling sering dilupakan: Event adalah sinyal, bukan UI concern — tapi UI boleh mendengar sinyal itu.
Artinya, kamu tidak desain Event untuk memenuhi kebutuhan frontend. Kamu desain Event berdasarkan logika bisnis. Frontend yang kemudian “menempelkan diri” ke Event itu lewat Echo — bukan sebaliknya.