مدیریت وابستگی ها در لاراول
در فریم ورک لاراول از ابزاری به نام Service Container به منظور مدیریت وابستگی ها و تزریق وابستگی ها استفاده می شود. تزریق وابستگی یعنی: وابستگی های یک کلاس از طریق سازنده (در برخی موارد از طریق از متدهای “setter”) به کلاس تزریق می شود. برای درک بهتر به مثال ساده زیر توجه کنید:
<?php namespace AppHttpControllers; use AppHttpControllersController; use AppRepositoriesUserRepository; use AppModelsUser; class UserController extends Controller { /** * The user repository implementation. * * @var UserRepository */ protected $users; /** * Create a new controller instance. * * @param UserRepository $users * @return void */ public function __construct(UserRepository $users) { $this->users = $users; } /** * Show the profile for the given user. * * @param int $id * @return Response */ public function show($id) { $user = $this->users->find($id); return view('user.profile', ['user' => $user]); } }
در مثال فوق UserController نیاز دارد تا اطلاعات کاربران را از یک منبع داده دریافت کند. بنابراین یک سرویسی که می تواند اطلاعات کاربران را در اختیارش بگذارد به آن تزریق شده است. در این نمونه سرویس UserRepository اغلب از Eloquent برای گرفتن اطلاعات کاربران از دیتابیس استفاده می کند اما چون سرویس UserRepository به این کلاس تزریق شده است به راحتی می توانیم آن را با پیاده سازی دیگری از سرویس که از منبع داده دیگری استفاده می کند، جایگزین کنیم. برای مثال در هنگام تست کردن پروژه به جای این که با دیتابس واقعی کار کنیم، می توانیم یک نمونه فیک از سرویس UserRepository ایجاد کرده و استفاده کنیم.
یادگیری عمیق Service Container و درک نحوه عملکرد آن در لاراول برای ساختن یک برنامه قدرتمند، بزرگ ضروری است.
در اکثر مواقع فقط با نوشتن نوع سرویس، آن سرویس به کلاس یا متد تزریق می شود و لازم نیست Service Container را برای چگونگی تزریق آن تنظیم کنید. برای مثال ممکن است در تعریف یک مسیر به یک نمونه از شیء IlluminateHttpRequest نیاز داشته باشید. برای دسترسی به آن کافیست مانند نمونه زیر یک ورودی از نوع Request تعریف کنید و Service Container به صورت خودکار آن را تزریق خواهد کرد.
use IlluminateHttpRequest; Route::get('/', function (Request $request) { // ... });
اغلب به لطف تزریق خودکار و facade ها می توانید یک برنامه کامل را بدون نیاز به تنظیم دستی نحوه تزریق وابستگی ها در Service Container ایجاد کنید. با وجود موارد گفته شده شاید این سوال پیش آید: پس چه موقع باید آن را به صورت دستی تنظیم کنیم؟ در این مواقع:
- زمانی که کلاسی دارید که یک اینترفیس را پیاده سازی کرده است و در کلاسی دیگر می خواهید یک نمونه از آن اینترفیس را تزریق کنید، باید به Service Container بگید که چگونه باید یک نمونه از آن اینترفیس را ایجاد کند. زیرا ممکن است چندین پیاده سازی از یک اینترفیس وجود داشته باشد.
- زمانی که یک بسته برای لاراول می نویسید و می خواهید آن بسته را با سایر توسعه دهنده ها به اشتراک بگذارید، باید سرویس های مربوط به بسته خود را در Service Container ثبت کنید.
بایند کردن (Binding)
بایند کردن ساده
تقریبا تمام موارد مربوط به بایند کردن سرویس ها و تنظیم Service Container در داخل ارائه دهندگان سرویس انجام می شود. در داخل یک ارائه دهنده سرویس با استفاده از $this->app می توانید به Container دسترسی داشته باشید. برای تنظیم یک سرویس در داخل Container از متد bind استفاده می شود. این متد دو ورودی دارد که اولی نام کلاس یا اینترفیس و دومی نحوه resolve کردن آن کلاس یا اینترفیس است. برای نمونه:
use AppServicesTransistor; use AppServicesPodcastParser; $this->app->bind(Transistor::class, function ($app) { return new Transistor($app->make(PodcastParser::class)); });
توجه داشته باشید که در داخل خود resolver هم به container دسترسی داریم (از طریق آرگومان $app). در نتیجه می توانیم با استفاده از وابستگی های مورد نیاز کلاس اصلی را دریافت کنیم. در نمونه فوق کلاس Transistor به کلاس PodcastParser وابستگی دارد و با استفاده از Container نمونه ای از آن برای خود ایجاد می کند. همانطور که گفته شد در اغلب موارد تعامل شما با Service Container در داخل ارائه دهندگان سرویس خواهد بود. با این حال اگر لازم داشتید می توانید مانند نمونه زیر با استفاده از App که یک facade است، سرویس های خود را در خارج از ارائه دهندگان سرویس تنظیم کنید:
use AppServicesTransistor; use IlluminateSupportFacadesApp; App::bind(Transistor::class, function ($app) { // ... });
توجه داشته باشید که اگر کلاسی به هیچ اینترفیسی وابستگی ندارد، لازم نیست آن را در Container بایند کنید چون فقط یک نوع از آن تعریف شده است و Container می تواند با استفاده از قابلیت reflection از آن ها نمونه سازی کند.
بایند کردن Singleton
متد singleton برای بایند کردن کلاس یا اینترفیسی استفاده می شود که فقط یک نمونه از آن باید ایجاد شود. به عبارت دیگر یک نمونه ایجاد می شود و اگر در جای دیگر لازم بود از همان نمونه قبلی استفاده می شود. نحوه استفاده از آن به این شکل است:
use AppServicesTransistor; use AppServicesPodcastParser; $this->app->singleton(Transistor::class, function ($app) { return new Transistor($app->make(PodcastParser::class)); });
بایند کردن Scoped Singleton
متد scoped برای بایند کردن کلاس یا اینترفیسی استفاده می شود که فقط باید یک نمونه از آن در طول یک محدوده خاص ایجاد شود. در لاراول این محدوده طول عمر یک request یا job است. کلاس ها یا اینترفیس هایی که با استفاده از این متد بایند شوند، در هنگام دریافت درخواست جدید دوباره نمونه سازی می شوند. به عبارت ساده تر محدود به پردازش یک درخواست هستند. نحوه استفاده از آن به این شکل است:
use AppServicesTransistor; use AppServicesPodcastParser; $this->app->scoped(Transistor::class, function ($app) { return new Transistor($app->make(PodcastParser::class)); });
بایند کردن نمونه ها
با استفاده از متد instance می توانید یک نمونه از قبل ایجاد شده را بایند کنید. با این کار هر زمان که آن سرویس درخواست شود، نمونه بایند شده بازگشت داده می شود. نحوه استفاده از آن به این شکل است:
use AppServicesTransistor; use AppServicesPodcastParser; $service = new Transistor(new PodcastParser); $this->app->instance(Transistor::class, $service);
بایند کردن اینترفیس به پیاده سازی
یکی از ویژگی های Service Container امکان بایند کردن یک اینترفیس به یک پیاده سازی خاص است. برای مثال فرض کنید یک اینترفیس با نام EventPusher و یک پیاده سازی از آن با نام RedisEventPusher داریم. با استفاده از متد bind می توانیم آن را مانند نمونه زیر در Container ثبت کنیم:
use AppContractsEventPusher; use AppServicesRedisEventPusher; $this->app->bind(EventPusher::class, RedisEventPusher::class);
با این کار به Container می گویم که هر موقع به یک پیاده سازی از EventPusher نیاز داشتیم، یک نمونه از RedisEventPusher را تزریق کند. مثال:
use AppContractsEventPusher; /** * Create a new class instance. * * @param AppContractsEventPusher $pusher * @return void */ public function __construct(EventPusher $pusher) { $this->pusher = $pusher; }
بایند کردن شرطی
گاهی اوقات ممکن است از یک اینترفیس پیاده سازی های مختلفی داشته باشید و در جای های مختلف بخواهید از پیاده سازی های خاصی استفاده کنید. برای مثال ما کنترلرهایی با نام های PhotoController، VideoController و UploadController داریم که همه آن ها به IlluminateContractsFilesystemFilesystem وابستگی دارند. حال فرض کنید می خواهیم PhotoController از درایور local استفاده کند و بقیه از s3. برای اینکار می توانیم از قابلیت Contextual Binding مانند نمونه زیر استفاده کنیم:
use AppHttpControllersPhotoController; use AppHttpControllersUploadController; use AppHttpControllersVideoController; use IlluminateContractsFilesystemFilesystem; use IlluminateSupportFacadesStorage; $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'); });
گاهی اوقات ممکن است کلاسی داشته باشید که به یک مقدار خاص مثلا از نوع integer وابستگی داشته باشد. برای تزریق این وابستگی می توانید از Contextual Binding مانند نمونه زیر استفاده کنید:
$this->app->when('AppHttpControllersUserController') ->needs('$variableName') ->give($value);
همچنین ممکن است گاهی اوقات کلاسی داشته باشید که به آرایه ای از نمونه های برچسب خورده (Tagged Instances) وابستگی داشته باشد. با استفاده از متد giveTagged می توانید نمونه هایی را که برچسب خاصی دارند را تزریق کنید. برای مثال:
$this->app->when(ReportAggregator::class) ->needs('$reports') ->giveTagged('reports');
اگر نیاز داشتید که مقداری را از تنظیمات برنامه تزریق کنید به راحتی می توانید با استفاده از متد giveConfig این کار را انجام دهید:
$this->app->when(ReportAggregator::class) ->needs('$timezone') ->giveConfig('app.timezone');
بایند کردن ورودی از نوع Variadics
گاهی اوقات ممکن است کلاسی داشته باشید که با استفاده از سازنده variadic آرایه ای از اشیاء را به عنوان ورودی بگرید. برای مثال:
<?php use AppModelsFilter; use AppServicesLogger; class Firewall { /** * The logger instance. * * @var AppServicesLogger */ protected $logger; /** * The filter instances. * * @var array */ protected $filters; /** * Create a new class instance. * * @param AppServicesLogger $logger * @param array $filters * @return void */ public function __construct(Logger $logger, Filter ...$filters) { $this->logger = $logger; $this->filters = $filters; } }
با استفاده از قابلیت Contextual Binding و متد give می توانیم این نوع وابستگی را نیز تزریق کنیم. برای نمونه:
$this->app->when(Firewall::class) ->needs(Filter::class) ->give(function ($app) { return [ $app->make(NullFilter::class), $app->make(ProfanityFilter::class), $app->make(TooLongFilter::class), ]; });
کد فوق را به شکل زیر هم می توان نوشت:
$this->app->when(Firewall::class) ->needs(Filter::class) ->give([ NullFilter::class, ProfanityFilter::class, TooLongFilter::class, ]);
برچسب زدن
گاهی اوقات ممکن است نیاز داشته باشید که سرویس های خاصی را دسته بندی کنید. فرض کنید می خواهید یک سرویس گزارش دهی ایجا کنید و پیاده سازی های مختلفی از اینترفیس Report را در Container ثبت کرده اید:
$this->app->bind(CpuReport::class, function () { // }); $this->app->bind(MemoryReport::class, function () { // });
بعد از ثبت آن ها می توانید با استفاده از متد tag بر روی آن ها برچسب بزنید. برای مثال دسته reports:
$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');
با این کار هر زمان که به پیاده سازی های فوق نیاز داشتید، می توانید با استفاده از متد tagged آن ها را دریافت کنید. برای نمونه:
$this->app->bind(ReportAnalyzer::class, function ($app) { return new ReportAnalyzer($app->tagged('reports')); });
به عبارت ساده تر با استفاده از قابلیت برچسب زدن می توانید سرویس های مختلف را دسته بندی کنید.
متد extend
این متد امکان اعمال تغییرات بر روی سرویس های resolve شده را فراهم می کند. برای مثال زمانی که یک سرویس resolve می شود ممکن است بخواهید قبل از رسیدن به دست درخواست کننده، تغییراتی بر روی آن اعمال کنید. متد extend یک closure به عنوان ورودی می گیرد که باید نسخه تغییر یافته سرویس را بازگرداند. این closure سرویس resolve شده و نمونه ای از Service Container را به عنوان ورودی دریافت می کند:
$this->app->extend(Service::class, function ($service, $app) { return new DecoratedService($service); });
متد make
متد make به منظور resolve کردن نمونه ای از یک کلاس از Container استفاده می شود. این متد نام کلاس یا اینترفیس را عنوان ورودی می گیرد و نمونه ای از آن را باز میگرداند:
use AppServicesTransistor; $transistor = $this->app->make(Transistor::class);
اگر سرویس مورد نظر برای نمونه سازی نیاز به ورودی خاصی داشته باشد می توانید از متد makeWith مانند نمونه زیر استفاده کنید:
use AppServicesTransistor; $transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);
همانطور که قبلا هم گفتیم برای دسترسی به Service Container در جایی که به آن به صورت $app دسترسی ندارید، می توانید از facade آن مانند نمونه زیر استفاده کنید:
use AppServicesTransistor; use IlluminateSupportFacadesApp; $transistor = App::make(Transistor::class);
اگر در داخل کلاسی به خود Container نیاز داشتید می توانید مانند سرویس های دیگر آن را تزریق کنید. مثال:
use IlluminateContainerContainer; /** * Create a new class instance. * * @param IlluminateContainerContainer $container * @return void */ public function __construct(Container $container) { $this->container = $container; }
فراخوانی متد و تزریق
گاهی اوقات ممکن است بخواهید یک متد از یک نمونه (شیء) را فراخوانی کنید در حالی که لازم دارید تا وابستگی های مورد نیازش نیز تزریق شود. برای مثال:
<?php namespace App; use AppRepositoriesUserRepository; class UserReport { /** * Generate a new user report. * * @param AppRepositoriesUserRepository $repository * @return array */ public function generate(UserRepository $repository) { // ... } }
برای اینکار می توانید مانند نمونه زیر از متد App::call استفاده کنید:
use AppUserReport; use IlluminateSupportFacadesApp; $report = App::call([new UserReport, 'generate']);
متد call می تواند وابستگی های مورد نیاز یک closure را نیز resolve کند. برای مثال:
use AppRepositoriesUserRepository; use IlluminateSupportFacadesApp; $result = App::call(function (UserRepository $repository) { // ... });
رویدادهای Container
در لاراول Service Container هر زمان که یک سرویس را resolve می کند، یک رویداد را به اصطلاح fire می کند. برای گوش دادن به این رویداد می توانید مانند نمونه زیر از متد resolving استفاده کنید:
use AppServicesTransistor; $this->app->resolving(Transistor::class, function ($transistor, $app) { // Called when container resolves objects of type "Transistor"... }); $this->app->resolving(function ($object, $app) { // Called when container resolves object of any type... });
نوشته مدیریت وابستگی ها در لاراول – آموزش لاراول اولین بار در سورس سرا – آموزش برنامه نویسی. پدیدار شد.