مرا اسکن کن!

Service Container در لاراول چیست

Service Container در لاراول چیست



از مهمترین پایه‌های لاراول 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 می‌نویسیم :

$this->app->bind('SmsNotification', SmsNotification::class);

خب با این کار ما سرویس (کلاس) رو به 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 استفاده می‌کنیم :

$user = new User;
$user->type = 'admin';
$user->permissions = 32;

$this->app->instance('AdminUser', $user);

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

$user = resolve('AdminUser');
$user->name = 'Calvin';

$user->save();

dd($user);

/*
array:3 [
  "name" => "Calvin",
  "type" => "admin",
  "permissions" => 32,
]
*/

Bind کردن اینترفیس‌ها

کلاس 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 استفاده می‌کنیم :

$this->app->resolving(function ($object, $app) {
    // ...
});

$this->app->resolving(Notification::class, function ($api, $app) {
    // ...
});

آیتم اولی ، برای هر سرویسی که 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);
}

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


نوشته شده توسط :

وحید صمدیان وحید صمدیان



شنبه, 23 مهر 1401

تعداد بازديد : 150

برچسب ها : فریم ورک لاراول

3.0 ستاره