سجاد نصیری
سجاد نصیری
خواندن ۷ دقیقه·۱ سال پیش

service container و تزریق وابستگی ها در laravel

مقدمه

در برنامه نویسی شی گرا گاهی پیش می آید که نیاز داریم از object یک class، در کلاس فعلی که در حال پیاده سازی آن هستیم استفاده کنیم. به طور کلی تزریق وابستگی(dependency-injection) یعنی نیازمندی های یک کلاس از طریق سازنده آن یا متدهای به اصطلاح setter به آن کلاس تزریق شوند تا بتوان درون کلاس از آن ها استفاده کرد.

هدف این نوشته اما بررسی تزریق وابستگی به شکل عام آن نیست و برای اطلاعات بیشتر راجع به این مفهوم می توانید از مقاله مفهوم تزریق وابستگی (Dependency Injection) - به زبان ساده استفاده کنید.

service provider ها در لاراول

لاراول برای مدیریت وابستگی ها از مفهومی به نام service container استفاده می کند. به طور کلی به 2 صورت می توان تزریق وابستگی ها را در لاراول انجام داد:

  1. Zero Configuration Resolution: یعنی نیازی به ساخت service provider و register کردن آن در config/app.php ندرایم و کافی است کلاس مورد نظر را به صورت type hint به ورودی یک تابع بدهیم تا بدون نیاز به new کردن، یک instance از آن در اختیار داشته باشیم. با ذکر مثالی درک این موضوع واضح تر خواهد شد، دو قطعه کدی که در پی خواهد آمد متناظر هستند و عملکرد مشابهی دارند:
<?php class Service { // ... } Route::get('/', function (Service $service) { $service->doSomeThing(); });

در کد بالا $service یک آبجکت از کلاس Service می باشد که می توان بدون استفاده از new آن را در بدنه تابع به کار بست. در واقع کد بالا معادل کد زیر می باشد:

<?php class Service { // ... } Route::get('/', function () { $service = new Service(); $service->doSomeThing(); });

اگرچه از روش بالا می توان در اکثر قسمت های برنامه لاراولی از جمله controller، middleware، event listener و... استفاده کرد اما ممکن است در برخی قسمت ها از جمله job ها، با مشکلاتی نیز مواجه شویم. همچنین ممکن است بخواهیم در طی فرایند تزریق وابستگی، طبق استراتژی خاصی interface را از implementation مستقل کنیم. کاری که در دیزاین پترن استراتژی هم انجام می دهیم. در چنین مواردی به اصطلاح لاراولی نیاز به bind کردن کلاس یا همان implementation به یک interface داریم. در لاراول این کار را با service provider ها انجام می دهیم که در ادامه آن را توضیح خواهیم داد.(در صورت نیاز میتوانید از طریق این لینک با interface آشنا شوید)

همچنین لازم به ذکر است که در view ها نمی توان از این مدلهای تزریق وابستگی استفاده کرد و در فایل های blade، باید از facade یا به طور دقیق تر از realtime facade ها استفاده کنیم که فعلا به آن نخواهیم پرداخت اما می توانید مستندات لاراول در باره facades را مطالعه کنید.

2. service providers: وظیفه اصلی پرووایدر ها در لاراول ثبت و پیکربندی نیازمندی های برنامه لاراولی است. سرویس هایی که ما از آن ها در کدهایمان استفاده می کنیم مانند کش، سشن، کوکی، دیتابیس و ... باید از قبل راه اندازی شده باشند تا برنامه‌ی ما کار بکند. برای این منظور لاراول پیش از این که به سراغ کدهایی که در کنترلرها و یا دیگر قسمت ها نوشته ایم برود، هنگام راه اندازی و boot شدن فریمورک مقدمات لازم و کانفیگ های مربوط به این سرویس ها را انجام داده و سپس آنها را در ظرفی به نام service container می ریزد تا در ادامه بتوانیم به راحتی از آنها در سرتاسر کدهایمان به صورت type hint یا facade استفاده کنیم.

مزیت مهم دیگری که bind کردن برای ما دارد، این است که implementation را از interface جدا می کنیم. برای مثال می توانیم مشخص کنیم که اگر اینترفیس Filesystem::class در کنترلر PhotoController فراخوانی شد، Storage::disk('local') برای ما برگردانده شود و اگر در VideoController یا UploadController فرخوانی شد Storage::disk('s3') را داشته باشیم:

use App\Http\Controllers\PhotoController; use App\Http\Controllers\UploadController; use App\Http\Controllers\VideoController; use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Support\Facades\Storage; $this->app->when(PhotoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('local'); }); $this->app->when([VideoController::class, UploadController::class]) ->needs(Filesystem::class) ->give(function () { return Storage::disk('s3'); });

اگر هنوز مطلب برایتان پرواضح نشده نگران نباشید چرا که در ادامه به تفصیل به ساخت و ثبت service provider در لاراول خواهیم پرداخت.

اولین مرحله ساخت یک service provider با دستور زیر است:

php artisan make:provider RiakServiceProvider

فایل ساخته شده به شکل زیر خواهد بود:

<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; class RiakServiceProvider extends ServiceProvider { /** * Register services. * * @return void */ public function register() { // } /** * Bootstrap services. * * @return void */ public function boot() { // } }

2 متد مهم register و boot در این فایل وجود دارد. در هر دو متد می توانیم عملیات bind کردن را انجام دهیم اما تفاوت متد register و boot در این است که متد register در زمان ثبت نام سرویس ها در service container اجرا می شود و این درحالی است که متد boot پس از ثبت شدن همه سرویس ها در service container اجرا خواهد شد. پس اگر سرویسی داریم که به سرویس های دیگری که می خواهیم در container ثبت کنیم وابسته است، باید عملیات binding را در متد boot انجام دهیم. راجع به نحوه bind کردن و تفاوت این 2 متد در ادامه ببیشتر توضیح خواهیم داد.

به طور کلی نحوه bind به شکل زیر می باشد:

public function register() { $this->app->singleton(ConnectionService::class, function (Application $app) { return new Connection(config('riak')); }); }

در مثال بالا interface با نام ConnectionService به کلاس Connection به اصطلاح bind یا متصل شده و از این پس هرگاه در طول برنامه به صورت type-hint که پیشتر توضیح دادیم ConnectionService را فراخوانی کنیم درواقع یک instance از کلاس Connection را فراخوانی کرده ایم. متد singleton که این کار را برای ما انجام می دهد، مشابه دیزاین پترن singleton تضمین می کند که فقط یک instance برای ما ساخته شود. می توانستیم به جای singleton از متد bind استفاده کنیم و interface و clouser را به آن پاس دهیم. روش های binding دیگری به جز singleton و bind نیز وجود دارد که می توانید در مستندات لاراول آنها را مطالعه کنید:

https://laravel.com/docs/10.x/container

https://laravel.com/docs/10.x/providers

این کار بسیار پیشرفته، هوشمندانه و باظرافت است که در هیچ فریمورک دیگری نظیر ندارد. جدا کردن interface از implementation یک مزیت مهم دیگر نیز برای ما دارد و آن این که می توانیم به زیبایی هر چه تمام تر از دیزاین پترن استراتژی استفاده کنیم:

public function register() { $this->app->singleton(ConnectionService::class, function (Application $app) { if(request()->connect_type == 'normal'){ return new NormalConnection(config('riak')); } elseif(request()->connect_type == 'fast'){ return new FastConnection(config('riak')); } else { return new SlowConnection(config('riak')); } }); }

در کد بالا گفته ایم که اگر پارامتر connect_type که در request جاری از سمت front برای ما ارسال شده، برابر normal بود کلاس NormalConnection در هنگام فراخوانی اینترفیس ConnectionService در کدها برگردانده شود و اگر برابر fast بود کلاس FastConnection و الی آخر. این دقیقا کاری است که در دیزاین پترن استراتژی انجام میدادیم.

اما تفاوت متد boot با متد register چیست؟ تفاوت اصلی در این است که متد boot هنگامی که همه سرویس ها در container ثبت شدند اجرا خواهد شد. پس در این متد به پرووایدر های ثبت شده دسترسی داریم. این در حالی است که در متد register ممکن است با فراخوانی یک پرووایدر با این خطا مواجه شویم که این سرویس وجود ندارد، چرا که هنوز در container ثبت نشده. برای مثال اگر خواستیم از پرووایدر ConnectionService یا پرووایدر ها و سرویس های دیگر لاراول در یک پرووایدر دیگر استفاده کنیم، باید این کار را در متد boot انجام دهیم:

<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use App\contracts\ConnectToCacheService; use App\contracts\ConnectionService; class ExternalCacheServiceProvider extends ServiceProvider { /** * Register services. * * @return void */ public function register() { // } /** * Bootstrap services. * * @return void */ public function boot() { $this->app->singleton(ConnectToCacheService::class, function (Application $app, ConnectionService $connect_service) { return $connect_service->chooseService('cache'); }); } }

همانطور که در بالا می بینید در پرووایدر جدید ExternalCacheServiceProvider، می خواهیم از یک پرووایدر دیگر(ConnectionService) استفاده کنیم و برای این کار به جای پیاده سازی متد register این کار را در متد boot انجام دادیم.

امیدوارم این مطلب برای شما مفید باشد. اگر سوالی داشتید، در قسمت نظرات بپرسید✌️

service container
backend developer
شاید از این پست‌ها خوشتان بیاید