
اگر چند ماهی با لاراول کار کرده باشید، احتمالا بارها با مفاهیمی مانند سرویس کانتینر، دیپندنسی اینجکشن، سرویس پرووایدر و فساد مواجه شدهاید. بسیاری از توسعهدهندگان در ابتدای مسیر، از این مفاهیم استفاده میکنند بدون اینکه دقیقا بدانند پشت صحنه چه اتفاقی در حال رخ دادن است.
برای مثال بارها این کد را نوشتهایم:
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 را ساخته است، کنترل این وابستگی را از دست دادهایم.
این دقیقا همان مشکلی است که دیپندنسی اینجکشن برای حل آن به وجود آمده است.
دیپندنسی اینجکشن یا تزریق وابستگی یک الگوی طراحی است که میگوید:
یک کلاس نباید وابستگیهای خود را ایجاد کند، بلکه باید آنها را از بیرون دریافت کند.
به جای این:
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 چگونه ساخته شده است.
تنها چیزی که برایش مهم است این است که یک آبجکت معتبر در اختیارش قرار گرفته باشد.
سه نوع رایج وجود دارد.
رایجترین روش:
class UserService { public function __construct( private Mailer $mailer ) { } }
لاراول عمدتاً از همین روش استفاده میکند.
public function store( Request $request ) { // }
یا:
public function send( Mailer $mailer ) { // }
وابستگی هنگام فراخوانی متد تزریق میشود.
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()
است.
نمونه:
$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 استفاده کن.
برای این کار از بایند استفاده میکنیم.
$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()
هر بار نمونه جدید ساخته میشود.
$this->app->bind( ReportService::class );
مثال:
$a = app(ReportService::class); $b = app(ReportService::class);
نتیجه:
$a !== $b
فقط یک نمونه در کل برنامه ساخته میشود.
$this->app->singleton( ReportService::class );
نتیجه:
$a === $b
برای سرویسهایی مانند:
تنظیمات
Logger
Cache Manager
Configuration Service
معمولا Singleton مناسب است.
مثال:
$this->app->singleton( SettingsService::class );
گاهی ساخت آبجکت پیچیده است.
$this->app->bind( PaymentGateway::class, function () { return new ZarinpalGateway( config('services.zarinpal.key') ); } );
در این حالت کنترل کامل ساخت شیء را داریم.
فرض کنید دو سرویس مختلف داریم.
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);
یکی از رایجترین کاربردهای سرویس کانتینر:
class UserController { public function store( Request $request ) { // } }
لاراول بهصورت خودکار Request را میسازد و تزریق میکند.
مثال دیگر:
public function index( UserService $service ) { return $service->all(); }
هیچ newای وجود ندارد.
همه چیز توسط کانتینر انجام میشود.
class UserController { public function __construct( private UserService $service ) { } }
این الگو در پروژههای بزرگ بسیار رایج است.
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 );
بدون نیاز به سرویس واقعی.
همین ویژگی باعث میشود پروژههای بزرگ قابل تست باقی بمانند.
گاهی چنین کدهایی دیده میشود:
class UserService { public function create() { $mailer = app( Mailer::class ); } }
بهتر است:
class UserService { public function __construct( Mailer $mailer ) { } }
وابستگیها شفاف باشند.
هر چیزی Singleton نیست.
اگر سرویس Stateful باشد ممکن است مشکلات عجیبی ایجاد شود.
بد:
public function __construct( ZarinpalGateway $gateway ) { }
بهتر:
public function __construct( PaymentGateway $gateway ) { }
این کار انعطافپذیری سیستم را افزایش میدهد.
خیر.
این یکی از سوءبرداشتهای رایج است.
اگر فقط یک پیادهسازی وجود دارد و احتمال تغییر کم است، تعریف اینترفیس صرفا بهخاطر «رعایت اصول» معمولا ارزش چندانی ایجاد نمیکند.
مثلا:
UserService
ممکن است اصلا نیازی به اینترفیس نداشته باشد.
اما برای بخشهایی مانند:
درگاه پرداخت
ارسال پیامک
ذخیرهسازی فایل
سرویسهای خارجی
استفاده از اینترفیس معمولا منطقی است.
سرویس کانتینر یکی از مهمترین اجزای معماری لاراول است و بخش بزرگی از سادگی توسعه در این فریمورک به همین قابلیت وابسته است. زمانی که دیپندنسی اینجکشن را درک میکنید، ناگهان بسیاری از رفتارهای به ظاهر جادویی لاراول قابل توضیح میشوند.
سرویس کانتینر مسئول ساخت آبجکتها، مدیریت وابستگیها و تزریق آنها به بخشهای مختلف برنامه است. به کمک آن میتوان کلاسهایی مستقلتر، تستپذیرتر و قابل توسعهتر ساخت. همچنین مفاهیمی مانند سرویس پرووایدر، فساد، Queue، میدلور، کامند و کنترلر همگی به نوعی بر سرویس کانتینر تکیه دارند.
اگر بخواهیم تنها یک نکته را از این مقاله به خاطر بسپاریم، آن نکته این است:
کلاسها نباید وابستگیهای خود را بسازند، آنها باید وابستگیهای موردنیازشان را دریافت کنند.
این ایده ساده، پایه بسیاری از معماریهای مدرن نرمافزار و یکی از دلایل اصلی تمیزی و نگهداری آسان پروژههای لاراولی است.