ویرگول
ورودثبت نام
مجتبی پاکزاد
مجتبی پاکزادبرای خوندن بیشتر تجربیات و مطالعاتم من رو در باورژن baversion.com دنبال کنید.
مجتبی پاکزاد
مجتبی پاکزاد
خواندن ۶ دقیقه·۱ روز پیش

سرویس کانتینر در لاراول

اگر چند ماهی با لاراول کار کرده باشید، احتمالا بارها با مفاهیمی مانند سرویس کانتینر، دیپندنسی اینجکشن، سرویس پرووایدر و فساد مواجه شده‌اید. بسیاری از توسعه‌دهندگان در ابتدای مسیر، از این مفاهیم استفاده می‌کنند بدون اینکه دقیقا بدانند پشت صحنه چه اتفاقی در حال رخ دادن است.

برای مثال بارها این کد را نوشته‌ایم:

public function __construct(UserRepository $repository) { $this->repository = $repository; }

و همه چیز به شکل جادویی کار کرده است. لاراول به‌صورت خودکار آبجکت موردنیاز را ساخته و در اختیار ما قرار داده است. اما سوال مهم اینجاست:

لاراول از کجا فهمید که باید یک نمونه از UserRepository بسازد؟

پاسخ این سوال ما را به قلب یکی از مهم‌ترین بخش‌های فریم‌ورک لاراول یعنی Service Container می‌رساند.

در این مقاله ابتدا دیپندنسی اینجکشن را از پایه درک می‌کنیم، سپس به سراغ سرویس کانتینر می‌رویم و در نهایت یاد می‌گیریم که چگونه از آن در پروژه‌های واقعی استفاده کنیم.


مشکل چیست؟ وابستگی‌های مستقیم

فرض کنید در حال ساخت یک سیستم ارسال ایمیل هستیم.

class UserService { private Mailer $mailer; public function __construct() { $this->mailer = new Mailer(); } public function register(array $data): void { // Register user $this->mailer->send( $data['email'], 'Welcome' ); } }

در نگاه اول مشکلی وجود ندارد.

اما UserService مستقیما به Mailer وابسته شده است.

اگر روزی تصمیم بگیریم سرویس ارسال ایمیل را تغییر دهیم چه؟

مثلا:

class AwsMailer { // }

حالا باید کلاس UserService را تغییر دهیم.

$this->mailer = new AwsMailer();

مشکل دوم زمانی ظاهر می‌شود که بخواهیم تست بنویسیم.

در تست‌ها معمولا نمی‌خواهیم واقعاً ایمیلی ارسال شود.

اما چون کلاس UserService خودش Mailer را ساخته است، کنترل این وابستگی را از دست داده‌ایم.

این دقیقا همان مشکلی است که دیپندنسی اینجکشن برای حل آن به وجود آمده است.


Dependency Injection چیست؟

دیپندنسی اینجکشن یا تزریق وابستگی یک الگوی طراحی است که می‌گوید:

یک کلاس نباید وابستگی‌های خود را ایجاد کند، بلکه باید آن‌ها را از بیرون دریافت کند.

به جای این:

class UserService { private Mailer $mailer; public function __construct() { $this->mailer = new Mailer(); } }

این را می‌نویسیم:

class UserService { public function __construct( private Mailer $mailer ) { } }

اکنون کلاس UserService دیگر مسئول ساخت Mailer نیست.

وابستگی از بیرون تزریق می‌شود.

به همین دلیل:

  • کلاس‌ها مستقل‌تر می‌شوند.

  • تست‌نویسی آسان‌تر می‌شود.

  • توسعه سیستم راحت‌تر می‌شود.

  • وابستگی‌ها قابل جایگزینی هستند.


درک دیپندنسی اینجکشن با یک مثال واقعی

فرض کنید یک فروشگاه اینترنتی داریم.

پس از ثبت سفارش باید پیامکی برای مشتری ارسال شود.

بدون دیپندنسی اینجکشن:

class OrderService { public function placeOrder(): void { $sms = new SmsService(); $sms->send(); } }

اما با دیپندنسی اینجکشن:

class OrderService { public function __construct( private SmsService $sms ) { } public function placeOrder(): void { $this->sms->send(); } }

در این حالت OrderService اصلا اهمیتی نمی‌دهد که SmsService چگونه ساخته شده است.

تنها چیزی که برایش مهم است این است که یک آبجکت معتبر در اختیارش قرار گرفته باشد.


انواع دیپندنسی اینجکشن

سه نوع رایج وجود دارد.

Constructor Injection

رایج‌ترین روش:

class UserService { public function __construct( private Mailer $mailer ) { } }

لاراول عمدتاً از همین روش استفاده می‌کند.

Method Injection

public function store( Request $request ) { // }

یا:

public function send( Mailer $mailer ) { // }

وابستگی هنگام فراخوانی متد تزریق می‌شود.

Setter Injection

class UserService { private Mailer $mailer; public function setMailer( Mailer $mailer ): void { $this->mailer = $mailer; } }

در لاراول کمتر مورد استفاده قرار می‌گیرد.


سرویس کانتینر چیست؟

حال سوال اصلی مطرح می‌شود.

وقتی می‌نویسیم:

public function __construct( UserRepository $repository ) { }

چه چیزی آبجکت UserRepository را می‌سازد؟

پاسخ:

Laravel Service Container

سرویس کانتینر یک کارخانه بزرگ ساخت آبجکت‌ها است.

وظیفه آن:

  • ساخت کلاس‌ها

  • مدیریت وابستگی‌ها

  • ریزالو کردن سرویس‌ها

  • نگهداری سینگلتون‌ها

است.


سرویس کانتینر چگونه کار می‌کند؟

فرض کنید این کلاس را داریم:

class UserRepository { }

و:

class UserService { public function __construct( UserRepository $repository ) { } }

وقتی لاراول بخواهد UserService را بسازد:

app(UserService::class);

ابتدا Constructor را بررسی می‌کند.

می‌بیند که به UserRepository نیاز دارد.

ابتدا UserRepository را می‌سازد.

سپس آن را به UserService تزریق می‌کند.

نتیجه:

$service = app(UserService::class);

بدون اینکه حتی یک new نوشته باشیم.


تابع app چیست؟

یکی از مهم‌ترین توابع کمکی لاراول:

app()

است.

نمونه:

$userService = app( UserService::class );

در واقع:

app()

یک میانبر برای دسترسی به سرویس کانتینر است.


ریزالو کردن سرویس‌ها

نمونه:

$logger = app( Logger::class );

یا:

$logger = resolve( Logger::class );

هر دو تقریبا یک کار انجام می‌دهند.


مشکل اینترفیس‌ها

حال به سناریوی حرفه‌ای‌تر می‌رسیم.

فرض کنید:

interface PaymentGateway { public function pay( int $amount ); }

و:

class ZarinpalGateway implements PaymentGateway { }

سپس:

class CheckoutService { public function __construct( PaymentGateway $gateway ) { } }

این بار لاراول با خطا مواجه می‌شود.

چرا؟

زیرا اینترفیس قابل نمونه‌سازی نیست.

new PaymentGateway();

غیرممکن است.

در نتیجه باید به سرویس کانتینر بگوییم:

هر وقت PaymentGateway خواسته شد، از ZarinpalGateway استفاده کن.


Bind کردن سرویس‌ها

برای این کار از بایند استفاده می‌کنیم.

$this->app->bind( PaymentGateway::class, ZarinpalGateway::class );

معمولا در Service Provider قرار می‌گیرد.

class AppServiceProvider extends ServiceProvider { public function register() { $this->app->bind( PaymentGateway::class, ZarinpalGateway::class ); } }

اکنون لاراول می‌داند هنگام درخواست PaymentGateway چه چیزی را بسازد.


تفاوت bind و singleton

دو متد بسیار مهم:

bind()

و

singleton()

bind

هر بار نمونه جدید ساخته می‌شود.

$this->app->bind( ReportService::class );

مثال:

$a = app(ReportService::class); $b = app(ReportService::class);

نتیجه:

$a !== $b

singleton

فقط یک نمونه در کل برنامه ساخته می‌شود.

$this->app->singleton( ReportService::class );

نتیجه:

$a === $b

چه زمانی از Singleton استفاده کنیم؟

برای سرویس‌هایی مانند:

  • تنظیمات

  • Logger

  • Cache Manager

  • Configuration Service

معمولا Singleton مناسب است.

مثال:

$this->app->singleton( SettingsService::class );

Binding با Closure

گاهی ساخت آبجکت پیچیده است.

$this->app->bind( PaymentGateway::class, function () { return new ZarinpalGateway( config('services.zarinpal.key') ); } );

در این حالت کنترل کامل ساخت شیء را داریم.


Contextual Binding چیست؟

فرض کنید دو سرویس مختلف داریم.

StripeGateway

و

ZarinpalGateway

سرویس اول:

AdminCheckoutService

باید از Stripe استفاده کند.

سرویس دوم:

UserCheckoutService

باید از زرین‌پال استفاده کند.

Contextual Binding برای همین سناریو است.

$this->app ->when(AdminCheckoutService::class) ->needs(PaymentGateway::class) ->give(StripeGateway::class);

و:

$this->app ->when(UserCheckoutService::class) ->needs(PaymentGateway::class) ->give(ZarinpalGateway::class);

Method Injection در کنترلرها

یکی از رایج‌ترین کاربردهای سرویس کانتینر:

class UserController { public function store( Request $request ) { // } }

لاراول به‌صورت خودکار Request را می‌سازد و تزریق می‌کند.


مثال دیگر:

public function index( UserService $service ) { return $service->all(); }

هیچ newای وجود ندارد.

همه چیز توسط کانتینر انجام می‌شود.


Constructor Injection در کنترلرها

class UserController { public function __construct( private UserService $service ) { } }

این الگو در پروژه‌های بزرگ بسیار رایج است.


تزریق وابستگی در Jobها

class ProcessOrderJob { public function handle( OrderService $service ) { // } }

هنگام اجرای Job نیز Service Container فعال است.


تزریق وابستگی در کامندها

class SyncUsers extends Command { public function handle( UserService $service ) { // } }

تزریق وابستگی در میدلورها

class CheckSubscription { public function __construct( SubscriptionService $service ) { } }

لاراول این وابستگی‌ها را نیز ریزالو می‌کند.


ارتباط سرویس کانتینر با سرویس پرووایدر

تقریبا تمام Bindingهای پروژه داخل Service Providerها ثبت می‌شوند.

مهم‌ترین پرووایدر AppServiceProvider است.

متد:

register()

برای ثبت سرویس‌ها استفاده می‌شود.

public function register() { $this->app->bind( UserRepositoryInterface::class, UserRepository::class ); }

تست‌پذیری: بزرگ‌ترین مزیت دیپندنسی اینجکشن

فرض کنید:

class OrderService { public function __construct( PaymentGateway $gateway ) { } }

در تست می‌توانیم نسخه Mock شده را تزریق کنیم.

$gateway = Mockery::mock( PaymentGateway::class );

سپس:

$service = new OrderService( $gateway );

بدون نیاز به سرویس واقعی.

همین ویژگی باعث می‌شود پروژه‌های بزرگ قابل تست باقی بمانند.


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

استفاده بیش از حد از app()

گاهی چنین کدهایی دیده می‌شود:

class UserService { public function create() { $mailer = app( Mailer::class ); } }

بهتر است:

class UserService { public function __construct( Mailer $mailer ) { } }

وابستگی‌ها شفاف باشند.

استفاده بی‌دلیل از Singleton

هر چیزی Singleton نیست.

اگر سرویس Stateful باشد ممکن است مشکلات عجیبی ایجاد شود.

وابستگی مستقیم به کلاس‌های Concrete

بد:

public function __construct( ZarinpalGateway $gateway ) { }

بهتر:

public function __construct( PaymentGateway $gateway ) { }

این کار انعطاف‌پذیری سیستم را افزایش می‌دهد.


آیا باید همیشه Interface تعریف کنیم؟

خیر.

این یکی از سوءبرداشت‌های رایج است.

اگر فقط یک پیاده‌سازی وجود دارد و احتمال تغییر کم است، تعریف اینترفیس صرفا به‌خاطر «رعایت اصول» معمولا ارزش چندانی ایجاد نمی‌کند.

مثلا:

UserService

ممکن است اصلا نیازی به اینترفیس نداشته باشد.

اما برای بخش‌هایی مانند:

  • درگاه پرداخت

  • ارسال پیامک

  • ذخیره‌سازی فایل

  • سرویس‌های خارجی

استفاده از اینترفیس معمولا منطقی است.


جمع‌بندی

سرویس کانتینر یکی از مهم‌ترین اجزای معماری لاراول است و بخش بزرگی از سادگی توسعه در این فریم‌ورک به همین قابلیت وابسته است. زمانی که دیپندنسی اینجکشن را درک می‌کنید، ناگهان بسیاری از رفتارهای به ظاهر جادویی لاراول قابل توضیح می‌شوند.

سرویس کانتینر مسئول ساخت آبجکت‌ها، مدیریت وابستگی‌ها و تزریق آن‌ها به بخش‌های مختلف برنامه است. به کمک آن می‌توان کلاس‌هایی مستقل‌تر، تست‌پذیرتر و قابل توسعه‌تر ساخت. همچنین مفاهیمی مانند سرویس پرووایدر، فساد، Queue، میدلور، کامند و کنترلر همگی به نوعی بر سرویس کانتینر تکیه دارند.

اگر بخواهیم تنها یک نکته را از این مقاله به خاطر بسپاریم، آن نکته این است:

کلاس‌ها نباید وابستگی‌های خود را بسازند، آن‌ها باید وابستگی‌های موردنیازشان را دریافت کنند.

این ایده ساده، پایه بسیاری از معماری‌های مدرن نرم‌افزار و یکی از دلایل اصلی تمیزی و نگهداری آسان پروژه‌های لاراولی است.

سرویس کانتینرdependency injectionلاراولfacade
۱
۰
مجتبی پاکزاد
مجتبی پاکزاد
برای خوندن بیشتر تجربیات و مطالعاتم من رو در باورژن baversion.com دنبال کنید.
شاید از این پست‌ها خوشتان بیاید