تسلط بر جداسازی مبتنی بر رویداد (Event-Driven Decoupling) در Laravel

کنترلرهای Laravel شما اغلب به محل انباشته شدن منطق تجاری (business logic) تبدیل می‌شوند.

شما با یک جریان ثبت‌نام ساده شروع می‌کنید. خیلی زود، اعلان‌های ایمیل، هشدارهای Slack، لاگ‌های حسابرسی (audit logs) و فراخوانی‌های API را به یک متد واحد اضافه می‌کنید. این کار باعث ایجاد یک کنترلر چاق (fat controller) می‌شود.

کنترلرهای چاق باعث شکننده شدن کد شما می‌شوند. تست کردن آن‌ها دشوار است و اصل تک‌مسئولیتی (Single Responsibility Principle) را نقض می‌کنند.

برای حل این مشکل نیازی به ابزارهای پیچیده‌ای مانند RabbitMQ ندارید. Laravel دارای یک سیستم رویداد (event system) داخلی است که اکثر نیازها را پوشش می‌دهد.

مشکل وابستگی شدید (tight coupling): اگر یک API خبرنامه کند باشد، فرآیند ثبت‌نام کاربر شما نیز کند می‌شود. اگر سرویس ایمیل با خطا مواجه شود، کل درخواست با شکست مواجه می‌شود.

راه حل: معماری مبتنی بر رویداد (Event-Driven Architecture).

رویدادها به عنوان یک لایه میانی عمل می‌کنند. کنترلر شما یک اقدام را اعلام می‌کند و شنونده‌ها (Listeners) به طور مستقل به آن اقدام واکنش نشان می‌دهند.

یک کنترلر سبک (lean controller) به این شکل است:

public function register(RegisterRequest $request)
{
    $user = User::create($request->validated());

    UserRegistered::dispatch($user);

    return response()->json(['message' => 'Success'], 201);
}

اکنون کنترلر فقط مسئول ذخیره‌سازی داده‌ها است و کاری به اثرات جانبی (side effects) ندارد.

شما سه مزیت اصلی به دست می‌آورید:

  • عملکرد (Performance): کاربران بلافاصله پاسخ را دریافت می‌کنند. وظایف سنگین با استفاده از اینترفیس ShouldQueue در پس‌زمینه اجرا می‌شوند.
  • تاب‌آوری (Resilience): اگر سرویسی از دسترس خارج شود، شنونده می‌تواند وظیفه را بدون مختل کردن برنامه اصلی، دوباره تلاش (retry) کند.
  • قابلیت گسترش (Extensibility): می‌توانید با افزودن یک شنونده جدید، ویژگی‌های جدیدی مانند اعلان‌های Push را اضافه کنید، بدون اینکه به کنترلر اصلی دست بزنید.

بهترین روش‌هایی که باید رعایت کنید:

  • بر اثرات جانبی تمرکز کنید: از رویدادها برای پردازش‌های پس از عملیات (post-processing) استفاده کنید. از آن‌ها برای منطق اصلی که باید فوراً انجام شود، استفاده نکنید.
  • از نام‌های توصیفی استفاده کنید: از نام‌هایی با زمان گذشته مانند OrderPlaced یا UserRegistered استفاده کنید. این نشان می‌دهد که آن اقدام قبلاً انجام شده است.
  • از انتزاع بیش از حد (over-abstraction) خودداری کنید: اگر قطعه کدی ساده است و فقط در یک جا استفاده می‌شود، فراخوانی یک تابع بهتر از استفاده از رویداد است.

برای تغییرات دیتابیس از Eloquent Observers استفاده کنید. برای اقدامات تجاری از Events استفاده کنید.

بازنویسی (Refactoring) به سمت رویدادها، موضوع پایداری است. این کار عیب‌یابی کد را آسان‌تر و تست کردن آن را سریع‌تر می‌کند.

همین امروز یک اثر جانبی مزاحم را در کنترلر خود انتخاب کرده و آن را به یک شنونده منتقل کنید.

این مثال را در محیط Sandbox امتحان کنید: https://onlinephp.io/c/1f7b2

فراتر از کنترلرهای چاق: تسلط بر جداسازی مبتنی بر رویداد در لاراول

در دنیای توسعه لاراول، همه ما این تجربه را داشته‌ایم: کنترلری که ابتدا ساده به نظر می‌رسد اما به آرامی به یک موجود عظیم و غیرقابل مدیریت تبدیل می‌شود. این همان چیزی است که ما به آن «کنترلر چاق» (Fat Controller) می‌گوییم.

کنترلر چاق چیست؟

یک کنترلر چاق، کنترلری است که مسئولیت‌های بسیار زیادی را بر عهده دارد. به جای اینکه فقط هدایت ترافیک را انجام دهد، شروع به اجرای منطق تجاری (Business logic)، ارسال ایمیل، تعامل با APIهای شخص ثالث و موارد دیگر می‌کند. این کار اصل تک‌مسئولیتی (Single Responsibility Principle) را نقض می‌کند.

پیامدهای کنترلرهای چاق

۱. دشواری در تست: تست کردن کنترلری که همه کارها را انجام می‌دهد، یک کابوس است. ۲. نگهداری دشوار: تغییر یک چیز کوچک ممکن است باعث خرابی چیزی کاملاً بی‌ربط شود. ۳. قابلیت استفاده مجدد پایین: شما نمی‌توانید منطق را بدون کپی کردن کد، در جاهای دیگر استفاده کنید.

راه حل: معماری مبتنی بر رویداد (Event-Driven Architecture)

به جای اینکه کنترلر را وادار کنیم همه کارها را انجام دهد، می‌توانیم از رویدادها (Events) و شنوندگان (Listeners) داخلی لاراول استفاده کنیم. این کار به ما اجازه می‌دهد تا عمل اصلی را از اثرات جانبی (Side effects) جدا کنیم.

نحوه عملکرد

۱. رویداد (Event): یک کلاس ساده که نشان‌دهنده اتفاقی است که رخ داده است (مثلاً OrderPlaced). ۲. شنونده (Listener): کلاسی که منتظر آن رویداد می‌ماند و وظیفه خاصی را انجام می‌دهد (مثلاً SendOrderConfirmationEmail).

مثال عملی

یک فرآیند ثبت‌نام کاربر را تصور کنید.

روش «چاق»

public function store(Request $request)
{
    $user = User::create($request->all());
    Mail::to($user)->send(new WelcomeMail($user));
    Log::info('New user registered: ' . $user->id);
    // ... منطق بیشتر
}

روش «جدا شده» (Decoupled)

ابتدا، رویداد را ایجاد کنید: php artisan make:event UserRegistered

سپس، آن را در کنترلر فراخوانی (dispatch) کنید:

public function store(Request $request)
{
    $user = User::create($request->all());
    event(new UserRegistered($user));
    return response()->json($user);
}

حالا، شنوندگانی برای مدیریت اثرات جانبی ایجاد کنید: php artisan make:listener SendWelcomeEmail --event=UserRegistered php artisan make:listener LogUserRegistration --event=UserRegistered

نتیجه‌گیری

با انتقال اثرات جانبی به شنوندگان، کنترلرهای شما سبک باقی می‌مانند، کد شما ماژولارتر می‌شود و اپلیکیشن شما برای مقیاس‌پذیری و تست بسیار آسان‌تر خواهد بود.