Laravel'de olaylar (events) ve dinleyiciler
Laravel'in olay sistemiyle yan etkileri ana akıştan nasıl ayırdığımı ve pratikte ne kazandığımı örneklerle anlatıyorum.
Bir kullanıcı kayıt olduğunda ne yapılması gerekiyor? Hoşgeldin e-postası gönder, kullanıcı aktivitesini kaydet, belki pazarlama servisine bildir. Bu adımların hepsini UserController@register metoduna yazmak kolay — ama birkaç ay sonra o metodun içi dolup taşıyor.
Laravel’in event ve listener sistemi, bu yan etkileri kayıt işleminin kendisinden ayırmak için var. Bu yazıda bu sistemi nasıl kullandığımı aktarıyorum.
Olay nedir?
Olay, uygulamada gerçekleşen anlamlı bir şeyi temsil eden basit bir PHP sınıfıdır. UserRegistered, OrderPlaced, PasswordChanged gibi. Olayın kendisi herhangi bir iş yapmaz; yalnızca “bu oldu” der ve ilgili veriyi taşır.
Listener, bu olayı karşılayan ve bir işi gerçekleştiren sınıftır. Bir olayın birden fazla listener’ı olabilir.
Kullanım örneği: kullanıcı kaydı
Önce olay sınıfını oluşturalım:
php artisan make:event UserRegistered
php artisan make:listener SendWelcomeEmail --event=UserRegistered
php artisan make:listener LogUserActivity --event=UserRegistered
Olay sınıfı, yalnızca taşıyacağı veriyi tutar:
<?php
namespace App\Events;
use App\User;
use Illuminate\Queue\SerializesModels;
class UserRegistered
{
use SerializesModels;
public User $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
İlk dinleyici e-posta gönderimi yapıyor:
<?php
namespace App\Listeners;
use App\Events\UserRegistered;
class SendWelcomeEmail
{
public function handle(UserRegistered $event): void
{
\Mail::send('emails.welcome', ['user' => $event->user], function ($m) use ($event) {
$m->to($event->user->email)
->subject('Hoşgeldiniz!');
});
}
}
İkinci dinleyici aktiviteyi kaydediyor:
<?php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\ActivityLog;
class LogUserActivity
{
public function handle(UserRegistered $event): void
{
ActivityLog::create([
'user_id' => $event->user->id,
'action' => 'registered',
]);
}
}
Olay servis sağlayıcısında bağlamak
EventServiceProvider içinde olayı dinleyicileriyle eşleştiriyoruz:
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
\App\Events\UserRegistered::class => [
\App\Listeners\SendWelcomeEmail::class,
\App\Listeners\LogUserActivity::class,
],
];
}
Controller’da olayı tetiklemek
Controller artık yalnızca kayıt işini yapıyor; yan etkilerle ilgilenmiyor:
<?php
namespace App\Http\Controllers;
use App\Events\UserRegistered;
use App\User;
use Illuminate\Http\Request;
class RegisterController extends Controller
{
public function store(Request $request)
{
$this->validate($request, [
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
$user = User::create([
'email' => $request->email,
'password' => bcrypt($request->password),
]);
event(new UserRegistered($user));
return redirect('/')->with('mesaj', 'Kaydınız tamamlandı!');
}
}
event(new UserRegistered($user)) satırı her şeyi başlatıyor. Controller bunun arkasında ne olduğunu bilmek zorunda değil.
Bu yapının kazancı nedir?
Yeni bir gereksinim geldiğinde — örneğin kayıt sonrası bir Slack bildirimi göndermek — yalnızca yeni bir dinleyici yazıp EventServiceProvider’a ekliyorum. Controller’a ve diğer dinleyicilere dokunmuyorum.
Ters yönde de geçerli: e-posta gönderme mantığını değiştirmem gerekirse yalnızca SendWelcomeEmail sınıfını açıyorum. Kayıt akışının geri kalanıyla hiçbir ilgisi yok.
Bir başka pratik kazanç: ShouldQueue interface’ini ekleyerek dinleyiciyi queue’ya alabiliyorsunuz, yani e-posta gönderimi arka planda işlenebiliyor. Bunu yapmak için listener’a tek bir şey eklemek yeterli:
use Illuminate\Contracts\Queue\ShouldQueue;
class SendWelcomeEmail implements ShouldQueue
{
// ...
}
Controller değişmiyor. Bu seviyedeki bir ayrıştırma olmadan bu kadar kolay olmazdı.
Dinleyicileri ayrı ayrı test etmek
Bu yapının test edilebilirlik açısından da bir kazancı var. Her dinleyici bağımsız bir sınıf olduğu için ayrı ayrı test edebiliyorsunuz. Gerçek UserRegistered olayını elle oluşturup dinleyiciyi doğrudan çalıştırmak mümkün:
public function test_hosgeldin_epostasi_gonderilir()
{
Mail::fake();
$user = User::factory()->create();
$listener = new SendWelcomeEmail();
$listener->handle(new UserRegistered($user));
Mail::assertSent(WelcomeMail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
}
Tüm kayıt akışını HTTP üzerinden tetiklemek gerekmeden yalnızca dinleyicinin doğru çalışıp çalışmadığını test ediyorsunuz. Bu, controller içine gömülü kod için mümkün olmayan bir yalıtım.
Olay isimlendirmesi önemli
Olay sınıfı adları uygulamanın dili haline geliyor. UserRegistered, OrderShipped, PaymentFailed gibi geçmiş zamanlı isimler, “bir şey oldu” anlamını net taşıyor ve kodu okuyan birinin sistemi anlamasını kolaylaştırıyor. DoSendEmail ya da HandleUserRegistration gibi isimler ise olayı dinleyiciyle karıştırıyor; oku bulanıklaşıyor.
Bu konuya başlangıçta dikkat etmemiştim; birkaç yanlış isimli olay sınıfı yarattıktan sonra farkettim ki isimlendirme kodu anlamak için gerçek bir ipucu. Olayı “ne oldu?” sorusuna cevap verecek şekilde adlandırmak, dinleyicileri de “bu olduğunda ne yapılmalı?” sorusuna göre adlandırmayı kolaylaştırıyor.
Dikkat ettiğim bir nokta
Olayları aşırı kullanmak takibi güçleştirebilir. Bir iş adımını her olayı ateşleyerek zincirlemeye başlarsanız, bir isteğin hangi akışı izlediğini anlamak için birkaç farklı dosyaya bakmanız gerekiyor. Yan etkileri ayırmak için iyi, birincil akışı dağıtmak için değil — bunu aklımda tutmaya çalışıyorum.
Basit bir kural edindim: bir controller metodu içinde kalıp da “bu işi buraya koyarsam metodun sorumluluğu büyür” diye düşünüyorsam olay sistemi devreye giriyor. Ama aynı akıştaki zorunlu iş adımlarını — örneğin sipariş oluştururken stok düşürme — olaya taşımak mantıklı değil. O adım olmadan iş tamamlanmış sayılamaz; onu dinleyiciye gömmek hem takibi güçleştiriyor hem de hata yönetimini karmaşıklaştırıyor. Olay sistemi, “olursa iyi ama zorunlu değil” ya da “sonradan eklenebilir” türünden yan etkiler için en verimli çalışıyor.