অধ্যায় ৭

Events & Observers

"Separate 'what happened' from 'what should follow'."

কিছু ঘটলে একটি event ফায়ার হয়, আর যার দরকার সে সাড়া দেয় — Model, Queue, Auth, Notification, Scheduler, HTTP Client সবখানে।

~১৩ মিনিট১২টি স্লাইড

সমস্যা: সব কন্ট্রোলারে

একজন ইউজার অর্ডার দিলো। এখন অনেক কিছু হতে হবে: কনফার্মেশন ইমেইল, ইনভেন্টরি আপডেট, ওয়্যারহাউসকে নোটিফাই, অডিটের জন্য লগ। সব কন্ট্রোলারে রাখা যায় — কিন্তু দ্রুত জগাখিচুড়ি হয়ে যায়। প্রতিটি নতুন রিকোয়ারমেন্টে checkout লজিক ছুঁতে হয়, মেথড এত বড় হয় যে কেউ কাছে যেতে চায় না।

Event ছাড়া side effect জমা হতে থাকে controller, model method বা service class-এ:

  • রেজিস্ট্রেশনের পর ইমেইল → registration মেথডের অংশ
  • মডেল আপডেটের পর search index sync → update মেথডে
  • অডিট ট্রেইল → যেখানে অ্যাকশন ঘটে সেখানেই

Tight Coupling

নতুন আচরণ মানে পুরোনো কোড বদলানো।

টেস্ট কঠিন

একটা জিনিস টেস্ট করতে সব side effect সামলাতে হয়।

বিশাল ক্লাস

অনেক কিছু করে বলে বোঝা কঠিন।

Event কী সমাধান করে

সহজ ভাষায়

Event হলো একটি সংকেত — “অর্ডার প্লেস হয়েছে”। Listener হলো সেই সংকেতের অপেক্ষায় বসে থাকা কোড — “মেইল পাঠাও”, “স্টক কমাও”। কন্ট্রোলার শুধু সংকেত দেয়; পরে কী হবে সে জানে না, পরোয়াও করে না।

Loose Coupling

যে event পাঠায় সে জানে না কে শুনছে।

পরিষ্কার বিভাজন

কী ঘটলো বনাম এরপর কী হবে।

সহজ এক্সটেনশন

মূল কোড না ছুঁয়ে listener যোগ/বাদ।

ঐচ্ছিক Async

ShouldQueue দিলেই listener হয়ে যায় queued job।

Event কীভাবে কাজ করে

  1. একটি event dispatch হয় — সাধারণত যা ঘটেছে তার বর্ণনা দেওয়া একটি object।
  2. Dispatcher ওই event-এর রেজিস্টার্ড listener খোঁজে।
  3. Listener চলে — synchronously বা asynchronously।
  4. Queued listener queue worker-এ যায়, retry ও backoff সাপোর্টসহ।

Event ডেটা বহন করে। Listener প্রতিক্রিয়া বহন করে।

নিজের কোডে: Event ও Listener

ধাপ ১: Event তৈরি

php artisan make:event OrderPlaced
class OrderPlaced
{
    use Dispatchable;

    public function __construct(
        public Order $order
    ) {}
}

ধাপ ২: Listener তৈরি

php artisan make:listener SendOrderConfirmation --event=OrderPlaced
class SendOrderConfirmation implements ShouldQueue
{
    public function handle(OrderPlaced $event): void
    {
        Mail::to($event->order->user)->send(new OrderReceipt($event->order));
    }
}

class UpdateInventoryLevels implements ShouldQueue
{
    public function handle(OrderPlaced $event): void
    {
        $event->order->items->each->reduceInventory();
    }
}

নিজের কোডে: Register ও Dispatch

ধাপ ৩: Listener রেজিস্টার

Laravel 11+: app/Listeners-এ থাকা listener-এর handle()-এ event type-hint করা থাকলে লারাভেল নিজেই খুঁজে নেয় (Event Discovery)। আলাদা রেজিস্টার লাগে না।

ম্যানুয়ালি করতে চাইলে AppServiceProvider::boot()-এ:

Event::listen(OrderPlaced::class, SendOrderConfirmation::class);
Event::listen(OrderPlaced::class, UpdateInventoryLevels::class);

পুরোনো ভার্সনে EventServiceProvider-এর $listen array:

protected $listen = [
    OrderPlaced::class => [
        SendOrderConfirmation::class,
        UpdateInventoryLevels::class,
    ],
];

ধাপ ৪: Event Dispatch

public function store(Request $request)
{
    $order = Order::create($request->validated());

    OrderPlaced::dispatch($order); // অথবা event(new OrderPlaced($order));

    return response('Order successful');
}

ফলাফল

Checkout লজিক না ছুঁয়েই পরে নতুন listener যোগ করা যায়। ইমেইল ও স্টক আপডেটের মতো ভারী কাজ asynchronously হয়। প্রতিটি listener একটাই কাজ করে, তাই সবকিছু testable।

Model Observers

Observer হলো Eloquent model-এর lifecycle hook-এ event প্যাটার্ন:

  • creating
  • created
  • updating
  • updated
  • saving
  • saved
  • deleting
  • deleted
  • restoring
  • restored
class UserObserver
{
    public function created(User $user): void
    {
        // নতুন ইউজার রেজিস্টার হলে ওয়েলকাম মেইল
        Mail::to($user)->send(new WelcomeEmail());
    }

    public function deleted(User $user): void
    {
        // ইউজার ডিলিট হলে প্রোফাইল পিকচার রিমুভ
        Storage::delete($user->avatar);
    }
}

Observer রেজিস্টার করা

use Illuminate\Database\Eloquent\Attributes\ObservedBy;

#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
    // ...
}

সাধারণ ব্যবহার: slug তৈরি, search index sync, সংশ্লিষ্ট cache ক্লিয়ার, পরিবর্তন লগ, domain event dispatch।

Observer মডেলকে হালকা রাখে। মডেল ডেটা ও রিলেশন ডিফাইন করে; observer প্রতিক্রিয়া সামলায়।

লারাভেল নিজেও Event ব্যবহার করে

লারাভেল এত সামঞ্জস্যপূর্ণ মনে হওয়ার একটা কারণ — ভেতরে অনেক সাবসিস্টেমে একই event প্যাটার্ন।

Queue

  • JobProcessing
  • JobProcessed
  • JobFailed
  • JobExceptionOccurred

Horizon এগুলো শুনে মেট্রিক্স ও ড্যাশবোর্ড বানায়।

Authentication

  • Registered
  • Login
  • Logout

অ্যাক্টিভিটি লগ, সন্দেহজনক আচরণ শনাক্ত, ওয়েলকাম ইমেইল।

Broadcasting

সাধারণ event + বাড়তি মেটাডেটা। লোকাল listener সার্ভারে সাড়া দেয়, broadcasting layer একই event websocket/Pusher দিয়ে ব্রাউজারে পাঠায়।

Notifications

  • NotificationSending
  • NotificationSent

ডেলিভারি লগ, ওপেন রেট ট্র্যাক।

Task Scheduling

  • ScheduledTaskStarting
  • ScheduledTaskFinished
  • ScheduledTaskSkipped
  • ScheduledTaskFailed

HTTP Client

  • RequestSending
  • ResponseReceived
  • ConnectionFailed

গ্লোবাল রিকোয়েস্ট লগ, পারফরম্যান্স মনিটরিং।

Context

  • ContextStarted
  • ContextFlushed
  • ContextUpdated

লম্বা প্রসেস বা background task-এ context পরিবর্তন দেখা।

ফ্রেমওয়ার্ক Event-এ হুক করা

আপনার নিজের event-এর মতোই লারাভেলের event শোনা যায় — ফ্রেমওয়ার্কের কোড না ছুঁয়ে।

Job ফেইল করলে Slack অ্যালার্ট

use Illuminate\Queue\Events\JobFailed;

Event::listen(function (JobFailed $event) {
    Log::channel('slack')->error('Job failed', [
        'job'   => $event->job->resolveName(),
        'error' => $event->exception->getMessage(),
    ]);
});

প্রতিটি লগইন অডিট করা

use Illuminate\Auth\Events\Login;

Event::listen(function (Login $event) {
    $event->user->update(['last_login_at' => now()]);
});

ধীর external API খুঁজে বের করা

use Illuminate\Http\Client\Events\ResponseReceived;

Event::listen(function (ResponseReceived $event) {
    if ($event->response->transferStats?->getTransferTime() > 2) {
        Log::warning('Slow API: ' . $event->request->url());
    }
});

সাধারণ সূত্র

প্রতিটি সাবসিস্টেম ভিন্ন সমস্যা সমাধান করে, কিন্তু একই কাঠামো: কিছু ঘটে → লারাভেল event পাঠায় → ডেভেলপার বা প্যাকেজ listener যুক্ত করে → কোনো tight coupling নেই।

Queued Listeners

Listener সাধারণত সাথে সাথে চলে। কিন্তু তার মানে এই না যে ছোট কাজেই সীমাবদ্ধ। Background job বানাতে শুধু একটা ধাপ — ShouldQueue interface:

class SendOrderConfirmation implements ShouldQueue
{
    public $tries = 3;           // অধ্যায় ২-এর retry এখানেও!
    public $queue = 'emails';    // কোন queue-তে যাবে

    public function handle(OrderPlaced $event): void
    {
        // ...
    }

    public function failed(OrderPlaced $event, Throwable $e): void
    {
        // সব চেষ্টা ব্যর্থ হলে
    }
}

লারাভেল সামলায়: queue-তে dispatch, serialization, retry, backoff, failure handling। রিকোয়েস্ট পারফরম্যান্স বাড়ানোর অন্যতম সহজ উপায় — কোড রিস্ট্রাকচার না করেই।

DB transaction-এর ভেতরে event dispatch করলে, commit-এর আগেই queued listener চলে যেতে পারে। নিরাপদ থাকতে listener-এ ShouldQueueAfterCommit ব্যবহার করুন।

কখন Event, কখন নয়

Event ভালো যখন

  • একই অ্যাকশনে একাধিক জিনিস সাড়া দেবে
  • পুরোনো কোড না বদলে এক্সটেনশন চান
  • ভারী কাজ মূল রিকোয়েস্ট থেকে সরাতে চান
  • প্রতিক্রিয়াগুলো স্বাধীন (ইমেইল, ইনডেক্সিং, অডিট লগ — নিজেদের মধ্যে সমন্বয় লাগে না)

Event ভালো নয় যখন

  • মূল business rule listener-এর ফলাফলের উপর নির্ভর করে
  • এত side effect যে event flow খুঁজে পাওয়া কঠিন
  • প্রতিক্রিয়ার কঠোর execution order লাগবে
  • কী ঘটলো তার তাৎক্ষণিক synchronous গ্যারান্টি দরকার

Event দিয়ে workflow সমৃদ্ধ করুন। জরুরি business logic ছড়ানো listener-এ লুকাবেন না — তখন control flow অদৃশ্য হয়ে যায়।

পুরো লারাভেলে Events

সাবসিস্টেমউদাহরণ event
Eloquent Modelcreated, updated, deleted (Observer)
QueueJobProcessed, JobFailed
AuthRegistered, Login, Logout
BroadcastingShouldBroadcast event
NotificationsNotificationSent
SchedulerScheduledTaskFailed
HTTP ClientResponseReceived, ConnectionFailed
ContextContextUpdated

মূল কথা

মেকানিজম সবসময় এক: event object dispatch করো, রেজিস্টার্ড listener সাড়া দেবে। “কী ঘটলো” থেকে “এরপর কী” আলাদা করা — ফ্রেমওয়ার্কের সবচেয়ে বেশি পুনঃব্যবহৃত কাঠামোগত আইডিয়াগুলোর একটি।

সারসংক্ষেপ ও যাচাই

  • Event = কী ঘটলো (ডেটা)। Listener = এরপর কী (প্রতিক্রিয়া)।
  • ShouldQueue দিলেই listener background job — retry/backoff সহ।
  • Observer = Eloquent lifecycle-এর জন্য event প্যাটার্ন।
  • লারাভেলের নিজের event (Queue, Auth, HTTP…) শুনে ফ্রেমওয়ার্ক না ছুঁয়ে মনিটরিং যোগ করা যায়।
  • জরুরি business logic listener-এ লুকাবেন না।

নিজেকে যাচাই করুন

অর্ডারের মোট দাম হিসাব করা কি listener-এ রাখা উচিত?

না। এটা মূল business logic, আর পরের ধাপ এর ফলাফলের উপর নির্ভর করে। Listener-এ রাখলে control flow অদৃশ্য হবে।

Horizon কীভাবে জানে কোন job সফল আর কোনটা ফেইল?

Queue worker-এর JobProcessed, JobFailed ইত্যাদি event শুনে।

Listener-কে async বানাতে কী লাগে?

শুধু implements ShouldQueue — আর একটি চলমান queue worker।