از مهمترین پایههای لاراول Service Container هست که توی این پست بطور مفصل با اون آشنا میشیم
یکی از مهمترین پایهها و نقات قوت لاراول Service Container هست به معنی یک مخزن یا یک مکان برای نگهداری سرویس (کلاس) های مورد نیاز برنامهی ما . یا به بیان فنیتر، یک ابزار برای مدیریت و تزریق وابستگیها (Dependency Injection) توی برنامه هست .
چرا به چنین مخزنی نیاز داریم ؟
برای مدیریت بهتر و جامعتر کلاسها و نمونههایی که از اونها ساخته میشن . اینطوری انسجام برنامه بالاتر میره. تست کردن که یکی از بخشهای جداییناپذیر هر برنامه هست هم راحتتر میشه .
Dependency Injection یا تزریق وابستگی چیه
همونطور که میدونیم توی برنامههای ما کلاسها به هم وابسته هستن . یک کلاس برای فعالیت خودش ممکنه به یک یا چند کلاس دیگه نیاز داشته باشه . خب این یعنی کلاسهای ما به هم وابسته هستن . کد زیر رو در نظر بگیرید :
class Car
{
public function make()
{
$wheel = new Wheel;
// ...
}
}
همونطور که میبینیم کلاس Car وابسته به کلاس Wheel (چرخ) هست . چون داره از این کلاس استفاده میکنه . اما این وابستگی شدید هست . یعنی اگه بخوایم نوع چرخ رو عوض کنیم باید همیشه کلاس Car رو ویرایش کنیم (نقض شدن اصل دوم سالید) . تست کردن کردن برنامه سخت میشه . چون کلاس Wheel ممکنه عملیاتی رو بخواد انجام بده که سنگین هست و یا توی محیط تست شدنی نیست . خب باید چکار کنیم ؟ یک راه استفاده از تکنیک تزریق وابستگی هست :
class Car
{
private $wheel;
public function __construct(Wheel $wheel)
{
$this->Wheel = $wheel;
}
public function make()
{
$this->wheel->create(4);
}
}
$car = new Car(new EconomicWheel);
$car->make();
توی کد بالا ما وابستگی کلاس Car رو از بیرون بهش تزریق کردیم . اینطوری میتونیم هر نوع چرخی رو به کلاس Car پاس بدیم بدون اینکه این کلاس رو دستکاری کنیم و همچنین تست برنامه راحتتر هست . چون میتونیم یک کلاس دلخواه (با رعایت اینترفیس) رو پاس بدیم . تزریق وابستگی از طریق constructor یا setter ها اتفاق میوفته .
خب توی مثال بالا ما وابستگی رو از بیرون ساختیم و به کلاس پاس دادیم . اگه تعداد وابستگیهای ما زیاد بشن چطور ؟ اگه کلاس Car به ۱۰ تا کلاس دیگه نیاز داشته باشه چطور ؟ باید موقع ساختن کلاس Car همهی این وابستگیها رو بسازیم ؟ یک راه بهتر ، مدیریت وابستگیها هست که یک فریمورک مثل لاراول این امکان رو به ما میده !
لاراول با استفاده از ابزاری به اسم Service Container و همچنین کلاسهای Reflection زبان PHP ، این کار رو بینهایت برای ما آسون کرده که توی ادامه با اون آشنا میشیم . پس Service Container ابزاری هست برای مدیریت وابستگیها توی برنامه .
تزریق خودکار
این نوع تزریق رو بارها توی فریمورک دیدیم :
namespace App\Http\Controllers;
class UsersController extends Controller
{
// ...
public function update(Request $request)
{
// ...
}
}
به پارامتر $request متد update دقت کنین . اینجا فریمورک متوجه میشه که پارامتر $request باید یک نمونه از کلاس Request باشه و این کار رو خودش بصورت خودکار انجام میده . به این کاری که لاراول انجام میده میگن resolve . یعنی فراهم کردن آبجکتهای مورد نیاز . پس لازم نیست مثل نمونه کد اول پست این کار رو دستی انجام بدیم .
خب این یک نمونه از تزریق خودکار بود . ما کلاسهای خودمون رو هم میتونیم به این صورت پاس بدیم . کلاس زیر رو در نظر بگیرید :
namespace App\Services;
class SmsNotification
{
public function send($user)
{
// ...
}
}
اگه کنترلرها، میدلورها و ... به این کلاس نیاز داشته باشن ، کافیه توی متدها بصورت type-hint از این کلاس استفاده کنیم :
namespace App\Http\Controllers;
// ... uses
class UsersController extends Controller
{
// ...
public function update(Request $request, SmsNotification $notification)
{
// ...
$notification->send($this->user);
}
}
توی کد بالا پارامتر $notification یک نمونه از کلاس SmsNotification هست که داریم از اون داخل متد استفاده میکنیم .
خب باید بدونیم که تزریق خودکار همه جای برنامه در دسترس نیست ! مثلا داخل یک کلاس شخصی توی یک پکیج . اگه بخوایم از تزریق وابستگی توی جاهایی استفاده کنیم که امکان تزریق خودکار وجود نداره ابتدا باید کلاس مورد نظرمون رو توی Service Container ثبت کنیم . برای این کار کلاس AppServiceProvider رو باز میکنیم و کد زیر رو داخل متد register مینویسیم :
خب با این کار ما سرویس (کلاس) رو به Container اضافه (به اصطلاح Bind) کردیم . توی آرگومان اول متد bind ما یک اسم دلخواه میدیم و آرگومان دوم اون سرویسی رو قرار میدیم که میخوایم اضافه بشه . برای استفاده از این سرویس توی برنامه ، باید اون رو resolve کنیم. برای این کار از تابع resolve مثل زیر استفاده میکنیم :
namespace Package\Name;
class HoneyMoon
{
// ...
protected function go()
{
$notif = resolve('SmsNotification');
$notif->send($this->user);
}
}
توی آرگومان اول تابع resolve باید اسمی که به سرویس نسبت دادیم بنویسیم .
خب اگه کلاس SmsNotification برای ساختهشدن نیاز به پارامتر داشته باشه چطور؟
namespace App\Services;
class SmsNotification
{
public function __construct($message)
{
$this->message = $message;
}
public function send($user)
{
// ...
}
}
ابتدا باید روش bind کردن توی AppServiceProvider رو تغییر بدیم :
$this->app->bind('SmsNotification', function($app, $parameters) {
return new SmsNotification($parameters[0]);
});
توی آرگومان دوم bind از یک کلوژر استفاده کردیم . پارامتر اول این کلوژر همیشه متغیر $app توسط فریمورک پاس داده میشه و پارامتر دوم ، آرگومانهایی هست که ما توسط تابع resolve به شکل زیر پاس میدیم :
namespace Package\Name;
class HoneyMoon
{
// ...
protected function go()
{
$notif = resolve('SmsNotification', ['Have fun']);
$notif->send($this->user);
}
}
همونطور که میبینیم آرگومانها رو باید بصورت آرایه پاس بدیم .
خب با این روش bind کردن ، هر دفعه که ما با resolve سرویسمون رو صدا میزنیم ، یک نمونه از کلاس SmsNotification ساخته میشه و به ما برگردونده میشه . اگه سرویسی داشته باشیم که نمونهسازی از اون هزینهی سنگینی داشته باشه مثلا کلاس اتصال به دیتابیس یا هارددیسک ، یک راه حل بهتر اینه که فقط یک نمونه از این سرویس ساخته بشه و توی هر فراخونی فقط اون تک نمونه برگردونده بشه . برای این کار هنگام ثبت توی Container از متد singletion استفاده میکنیم :
$this->app->singleton('HeavyTask', function ($app) {
return new HeavyTask;
});
یه وقتایی هست که از قبل یک نمونه از یک کلاس داریم و اون رو میخوایم bind کنیم . هنگام ثبت از متد instance استفاده میکنیم :
کلاس SmsNotification رو نظر بگیرید که توی کنترلر اینطوری از اون استفاده میکردیم :
namespace App\Http\Controllers;
class UsersController extends Controller
{
public function index(SmsNotification $notification)
{
}
}
خب اگه بعدا بخوایم بجای SMS ایمیل بدیم چطور ؟ کلاس UsersController باید تغییر و دستکاری بشه . برای جلوگیری از اینکار یک راه حل کلی استفاده از اینترفیسها هست :
namespace App\Http\Controllers;
use App\Contracts\Notification;
class UsersController extends Controller
{
public function index(Notification $notification)
{
}
}
ما یک اینترفیس رو جایگزین کلاس SmsNotification توی متد index کردیم تا بتونیم از روشهای دیگه مثل ایمیل هم استفاده کنیم . خب اگه این کد رو اجرا کنیم خطا میگیریم که یک اینترفیس قابل پیادهسازی نیست . باید به لاراول بگیم که وقتی هر جا این اینترفیس قراره resolve بشه ، به ما سرویس دلخواه ما رو برگردون . توی متد register کلاس AppServiceProvider مینویسیم :
$this->app->bind('App\Contracts\Notification', function() {
return new EmailNotification;
});
خب با این کار چه توی تزریق خودکار از طریق type-hint پارامتر متدها و چه با استفاده از تابع resolve وقتی این اینترفیس فراخونی میشه ، کلاس مورد نظر ما که اینجا EmailNotification هست رو برمیگردونه :
class UsersController extends Controller
{
public function index(Notification $notification)
{
dd($notification); // App\Services\EmailNotification
}
}
دقت کنین که کلاسهای EmailNotification و SmsNotification باید اون اینترفیس رو پیادهسازی کرده باشن .
خب توی مثال قبل گفتیم اگه اون اینترفیس فراخونی شد کلاس EmailNotification رو برگردون . اما فرض کنین توی یک کنترلر هنگام استفاده از اون اینترفیس به SmsNotification احتیاج داریم . برای این هم یک راه حل وجود داره که بهش میگن Contextual Binding و نحوهی تعریف کردن اون بصورت زیر هست :
$this->app->when(HomeController::class)
->needs(Notification::class)
->give(function() {
return new SmsNotification;
});
هنگام Bind کردن به فریمورک میگیم که اگه این اینترفیس توی فلان کلاس استفاده شد ، کلاس SmsNotification رو به ما برگردون . نکتهای که باید درنظر داشته باشیم اینه کلاس SmsNotification به constructor کلاس HomeController پاس داده میشه و توی بقیه متدها از بصورت type-hint یا resolve قابل دسترسی نیست :
// uses ...
class HomeController extends Controller
{
private $notification;
public function __construct(Notification $notification)
{
$this->notification = $notification;
}
public function index()
{
dd($this->notification); // App\Services\SmsNotification
}
}
رویدادها
شاید لازم باشه وقتی یک سرویس درحال resolve شدن هست یک سری عملیات رو اجرا کنیم . برای این کار از متد resolving استفاده میکنیم :
آیتم اولی ، برای هر سرویسی که resolve بشه اجرا میشه . برای یک رویداد خاص میتونیم از آیتم دوم استفاده کنیم .
توسعهی یک سرویس Bind شده
مواقعی پیش میاد که یک سرویس از قبل Bind شده (مثلا توی یک پرووایدر دیگه) و میخوایم این سرویس رو توسعه بدیم، بهش امکانات اضافه کنیم و اون پیکربندی کنیم . برای این کار از متد extend استفاده میکنیم :
$this->app->extend(Service::class, function ($service, $app) {
return new DecoratedService($service);
});
آرگومان دوم متد extend یک کلوژر هست که باید سرویس ویرایش شده رو برگردونه .
Binding های گروهی
اگه لازم باشه چند سرویس Bind شده رو توی یک گروه قرار بدیم ، از متد tag استفاده میکنیم :
$this->app->bind('Email', function () {
//
});
$this->app->bind('SMS', function () {
//
});
$this->app->tag(['Email', 'SMS'], 'notifications');
و به صورت زیر از این آیتمهای گروهبندی شده استفاده میکنیم :
$notifications = $this->app->tagged('notifications');
// or where $app isn't available:
$notifications = app()->tagged('notifications');
foreach ($notifications as $item) {
dump($item);
}
خب دوستان این موضوع علاوه بر مهم بودن ، یکی از راحتترین و سادهترین موضوعهایی هست که میتونیم از اون استفاده کنیم . فقط کافیه مفاهیم رو درک کرده باشیم .