Service Container و Dependency Injection در لاراول — راهنمای کامل برای همه

شاید اسم Service Container و Dependency Injection توی لاراول برات سنگین به نظر بیاد، اما واقعیت اینه که این مفاهیم خیلی هم ترسناک نیستن. اگه باهاشون آشنا بشی، می‌فهمی که بیشتر برای راحت‌تر کردن زندگی برنامه‌نویس‌ها ساخته شدن. توی این مقاله می‌خوام کامل و قدم به قدم توضیح بدم که اینا چی هستن، چرا مهمن و چطور باید ازشون استفاده کنیم.

Dependency Injection یعنی چی؟

Dependency Injection یا به اختصار DI یعنی "وابستگی‌ها رو به جای اینکه خودمون بسازیم، از بیرون دریافت کنیم". ساده‌تر بگم: فرض کن کلاس OrderService برای ذخیره‌ی سفارش نیاز داره به یک "ذخیره‌کننده‌ی دیتابیس". راه ساده و اشتباه:


class OrderService {
    public function __construct()
    {
        $this->repo = new OrderRepository();
    }
}
  

مشکل اینجاست که حالا کلاس ما "قفل" شده روی OrderRepository. اگه روزی بخوای از MongoDB استفاده کنی یا دیتابیس رو عوض کنی، باید بیای این کد رو تغییر بدی. یعنی انعطاف‌پذیری صفر!

روش درست:


class OrderService {
    public function __construct(OrderRepositoryInterface $repo)
    {
        $this->repo = $repo;
    }
}
  

اینجا OrderService فقط می‌گه من به چیزی نیاز دارم که مثل OrderRepositoryInterface کار کنه. حالا اینکه پشت صحنه دیتابیس MySQL باشه، MongoDB یا حتی یک API خارجی، دیگه مهم نیست. همه چیز از بیرون تزریق می‌شه.

Service Container چه نقشی داره؟

لاراول یک "جعبه ابزار هوشمند" به اسم Service Container داره. وظیفه‌ی اون اینه که وقتی کلاسی به چیزی نیاز داشت (مثلاً همون OrderRepositoryInterface) تصمیم بگیره چی باید ساخته بشه و اون رو آماده کنه.

توی فایل AppServiceProvider می‌تونیم بگیم:


$this->app->bind(
    App\Contracts\OrderRepositoryInterface::class,
    App\Repositories\EloquentOrderRepository::class
);
  

از این به بعد هر وقت لاراول دید یک کلاس OrderRepositoryInterface می‌خواد، خودش به طور خودکار EloquentOrderRepository رو می‌سازه و تزریق می‌کنه.

مثال واقعی: پرداخت آنلاین

فرض کن یک وبسایت فروشگاهی داری. توی حالت عادی توی محیط تست (sandbox) نمی‌خوای پول واقعی جابه‌جا بشه، ولی توی محیط اصلی باید به بانک وصل بشی. اینجاست که کانتینر به دادت می‌رسه:


$this->app->bind(PaymentGateway::class, function ($app) {
    if ($app->environment('production')) {
        return new StripePaymentGateway(config('services.stripe.key'));
    }
    return new FakePaymentGateway();
});
  

حالا هرجا PaymentGateway خواسته بشه، بسته به محیط (تست یا اصلی) لاراول تصمیم می‌گیره کدوم رو تزریق کنه. بدون اینکه حتی یک خط از کدت رو تغییر بدی!

bind یا singleton؟

اینجا دو روش برای تعریف وابستگی‌ها داریم:

  • bind: هر بار که نیاز شد، یک نمونه‌ی جدید ساخته می‌شه.
  • singleton: فقط یک بار ساخته می‌شه و بعدش همیشه همون استفاده می‌شه.

مثلاً برای یک کلاینت API یا اتصال Redis بهتره singleton باشه چون ساختنش گرونه و نیازی نیست هر بار نمونه‌ی جدید ساخته بشه.

مزایای استفاده از DI و Container

  • کد تمیزتر و قابل‌خواندن‌تر می‌شه.
  • تست‌نویسی راحت‌تر می‌شه. می‌تونی نسخه‌های فیک رو تزریق کنی.
  • تغییرات آینده آسون‌تره. لازم نیست توی کل پروژه دنبال new بزنی.
  • مدیریت پروژه‌های بزرگ ساده‌تر می‌شه. مخصوصاً وقتی تیم چندنفره داری.

تست‌نویسی راحت‌تر

یکی از بهترین جاهایی که ارزش DI رو می‌فهمی تست‌نویسیه. مثلاً برای تست Checkout می‌تونی PaymentGateway رو با یک نسخه‌ی فیک جایگزین کنی:


$this->app->bind(PaymentGateway::class, fn () => new FakePaymentGateway('test'));

$response = $this->post('/checkout', ['amount' => 5000]);
$response->assertOk()->assertJsonStructure(['transaction_id']);
  

اینجوری بدون اینکه به بانک وصل بشی یا پول واقعی جابه‌جا کنی، می‌تونی سیستم رو تست کنی.

خطاهای رایج

  • استفاده زیاد از new داخل کلاس‌ها. اینطوری وابستگی‌ها قفل می‌شن.
  • تزریق مستقیم پیاده‌سازی‌ها به‌جای اینترفیس. همیشه به اینترفیس کدنویسی کن.
  • استفاده بی‌جا از Singleton برای سرویس‌هایی که باید چند نمونه جدا داشته باشن.
  • قرار دادن منطق زیاد توی کنترلرها به‌جای انتقال به سرویس‌ها.

جمع‌بندی

Service Container و Dependency Injection اولش ممکنه کمی پیچیده به نظر برسن، ولی در عمل فقط یک روش برای داشتن کدی مرتب، تست‌پذیر و قابل‌گسترش هستن. با این کار پروژه‌های کوچیکت تمیزتر می‌شن و پروژه‌های بزرگ بدون این که شلوغ بشن به راحتی رشد می‌کنن.