نسخه:

کانتینر سرویس

معرفی

کانتینر سرویس لاراول یک ابزار قدرتمند برای مدیریت وابستگی های کلاس و انجام تزریق وابستگی است. تزریق وابستگی یک عبارت فانتزی است که اساساً به این معنی است: وابستگی‌های کلاس از طریق سازنده یا در برخی موارد متدهای «setter» به کلاس «تزریق» می‌شوند.

بیایید به یک مثال ساده نگاه کنیم:

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use App\Models\User;
use Illuminate\View\View;
 
class UserController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
protected UserRepository $users,
) {}
 
/**
* Show the profile for the given user.
*/
public function show(string $id): View
{
$user = $this->users->find($id);
 
return view('user.profile', ['user' => $user]);
}
}

در این مثال، UserController نیاز به بازیابی کاربران از یک منبع داده است. بنابراین، ما سرویسی را تزریق خواهیم کرد که قادر به بازیابی کاربران باشد. در این زمینه، ما UserRepository به احتمال زیاد از Eloquent برای بازیابی اطلاعات کاربر از پایگاه داده استفاده می کنیم. با این حال، از آنجایی که مخزن تزریق می شود، می توانیم به راحتی آن را با پیاده سازی دیگری تعویض کنیم. ما همچنین می‌توانیم به راحتی «تقلید» کنیم، یا یک پیاده‌سازی ساختگی از آن UserRepository هنگام آزمایش برنامه‌مان ایجاد کنیم.

درک عمیق کانتینر سرویس لاراول برای ساختن یک برنامه قدرتمند و بزرگ و همچنین برای کمک به هسته لاراول ضروری است.

وضوح تنظیمات صفر

اگر یک کلاس هیچ وابستگی نداشته باشد یا فقط به کلاس‌های بتن دیگر (نه واسط) وابسته باشد، نیازی نیست که ظرف در مورد نحوه حل آن کلاس آموزش داده شود. به عنوان مثال، می توانید کد زیر را در routes/web.php فایل خود قرار دهید:

<?php
 
class Service
{
// ...
}
 
Route::get('/', function (Service $service) {
die($service::class);
});

/ در این مثال، ضربه زدن به مسیر برنامه شما به طور خودکار Service کلاس را حل می کند و آن را به کنترلر مسیر شما تزریق می کند. این بازی در حال تغییر است. این بدان معنی است که می توانید برنامه خود را توسعه دهید و از مزایای تزریق وابستگی بدون نگرانی در مورد فایل های پیکربندی متورم استفاده کنید.

خوشبختانه، بسیاری از کلاس‌هایی که هنگام ساخت یک برنامه لاراول می‌نویسید، به‌طور خودکار وابستگی‌های خود را از طریق کانتینر دریافت می‌کنند، از جمله کنترلرها ، شنوندگان رویداد ، میان‌افزار و غیره. علاوه بر این، می‌توانید وابستگی‌هایی را در handle روش مشاغل در صف تایپ کنید . وقتی قدرت تزریق وابستگی پیکربندی خودکار و صفر را بچشید، توسعه بدون آن غیرممکن به نظر می رسد.

زمان استفاده از کانتینر

به لطف وضوح پیکربندی صفر، شما اغلب وابستگی‌های مسیرها، کنترل‌کننده‌ها، شنوندگان رویداد و جاهای دیگر را بدون تعامل دستی با کانتینر تایپ می‌کنید. برای مثال، ممکن است شی را در تعریف مسیر خود تایپ کنید Illuminate\Http\Request تا بتوانید به راحتی به درخواست فعلی دسترسی پیدا کنید. حتی اگر برای نوشتن این کد هرگز مجبور به تعامل با کانتینر نیستیم، اما در حال مدیریت تزریق این وابستگی ها در پشت صحنه است:

use Illuminate\Http\Request;
 
Route::get('/', function (Request $request) {
// ...
});

در بسیاری از موارد، به لطف تزریق وابستگی خودکار و نماها ، می‌توانید برنامه‌های لاراول را بدون اتصال دستی یا حذف چیزی از ظرف بسازید. بنابراین، چه زمانی می خواهید به صورت دستی با ظرف تعامل داشته باشید؟ بیایید دو موقعیت را بررسی کنیم.

ابتدا، اگر کلاسی بنویسید که یک اینترفیس را پیاده‌سازی می‌کند و می‌خواهید آن رابط را در یک مسیر یا سازنده کلاس تایپ کنید، باید به کانتینر بگویید که چگونه آن رابط را حل کند . ثانیاً، اگر در حال نوشتن یک بسته لاراول هستید که قصد دارید آن را با سایر توسعه دهندگان لاراول به اشتراک بگذارید، ممکن است لازم باشد خدمات بسته خود را به کانتینر متصل کنید.

الزام آور

مبانی صحافی

اتصالات ساده

تقریباً تمام اتصالات کانتینر خدمات شما در ارائه دهندگان خدمات ثبت می شود ، بنابراین بیشتر این نمونه ها استفاده از کانتینر را در آن زمینه نشان می دهند.

در یک ارائه دهنده خدمات، شما همیشه از طریق دارایی به کانتینر دسترسی دارید $this->app . می‌توانیم با استفاده از متد bind ، کلاس یا نام رابطی را که می‌خواهیم ثبت کنیم، به همراه یک بسته که نمونه‌ای از کلاس را برمی‌گرداند، ثبت کنیم:

use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->bind(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

توجه داشته باشید که ما خود ظرف را به عنوان آرگومان برای حل کننده دریافت می کنیم. سپس می‌توانیم از کانتینر برای رفع وابستگی‌های فرعی شیئی که می‌سازیم استفاده کنیم.

همانطور که گفته شد، شما معمولاً با کانتینر در ارائه دهندگان خدمات تعامل خواهید داشت. با این حال، اگر می‌خواهید با کانتینر خارج از یک ارائه‌دهنده خدمات تعامل داشته باشید، می‌توانید این کار را از طریق App نما انجام دهید :

use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;
 
App::bind(Transistor::class, function (Application $app) {
// ...
});

فقط در صورتی می توانید از این bindIf روش برای ثبت یک کانتینر binding استفاده کنید که قبلاً برای نوع داده شده یک اتصال ثبت نشده باشد:

$this->app->bindIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

اگر کلاس ها به هیچ واسطی وابسته نباشند، نیازی به اتصال کلاس ها به کانتینر نیست. ظرف نیازی به آموزش نحوه ساخت این اشیاء ندارد، زیرا می تواند به طور خودکار این اشیاء را با استفاده از بازتاب حل کند.

Binding A Singleton

متد singleton یک کلاس یا اینترفیس را به کانتینر متصل می کند که فقط یک بار باید حل شود. پس از حل شدن یک اتصال تکی، همان نمونه شیء در فراخوانی های بعدی به کانتینر برگردانده می شود:

use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->singleton(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

فقط در صورتی می‌توانید از این singletonIf روش برای ثبت صحافی ظرف تک تنی استفاده کنید که قبلاً برای نوع معین صحافی ثبت نشده باشد:

$this->app->singletonIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

صحافی Scoped Singletons

این scoped روش یک کلاس یا اینترفیس را به کانتینر متصل می‌کند که فقط باید یک بار در طول چرخه عمر درخواست/کار لاراول معین حل شود. در حالی که این روش مشابه singleton روش است، نمونه‌هایی که با استفاده از این scoped متد ثبت می‌شوند، هر زمان که برنامه لاراول یک "چرخه حیات" جدید را شروع کند، پاک می‌شوند، مانند زمانی که یک کارگر لاراول اکتان درخواست جدیدی را پردازش می‌کند یا زمانی که یک کارگر صف لاراول یک کار جدید را پردازش می‌کند:

use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->scoped(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

موارد الزام آور

همچنین می‌توانید یک نمونه شی موجود را با استفاده از روش به کانتینر متصل کنید instance . نمونه داده شده همیشه در تماس های بعدی به کانتینر برگردانده می شود:

use App\Services\Transistor;
use App\Services\PodcastParser;
 
$service = new Transistor(new PodcastParser);
 
$this->app->instance(Transistor::class, $service);

اتصال رابط به پیاده سازی

یکی از ویژگی های بسیار قدرتمند کانتینر سرویس، توانایی آن در اتصال یک رابط به یک پیاده سازی معین است. به عنوان مثال، فرض کنید یک EventPusher رابط و یک پیاده سازی داریم RedisEventPusher . هنگامی که پیاده سازی خود را از این رابط کدگذاری کردیم RedisEventPusher ، می توانیم آن را در کانتینر سرویس مانند زیر ثبت کنیم:

use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
 
$this->app->bind(EventPusher::class, RedisEventPusher::class);

این دستور به کانتینر می‌گوید که باید در RedisEventPusher زمانی که یک کلاس به پیاده‌سازی نیاز دارد، آن را تزریق کند EventPusher . اکنون می توانیم EventPusher اینترفیس را در سازنده کلاسی که توسط کانتینر حل می شود تایپ کنیم . به یاد داشته باشید، کنترل‌کننده‌ها، شنوندگان رویداد، میان‌افزار، و انواع مختلف کلاس‌های دیگر در برنامه‌های لاراول همیشه با استفاده از کانتینر حل می‌شوند:

use App\Contracts\EventPusher;
 
/**
* Create a new class instance.
*/
public function __construct(
protected EventPusher $pusher
) {}

پیوند متنی

گاهی اوقات ممکن است دو کلاس داشته باشید که از یک رابط استفاده می کنند، اما می خواهید پیاده سازی های مختلفی را به هر کلاس تزریق کنید. برای مثال، دو کنترل کننده ممکن است به پیاده سازی های مختلف قرارداد وابسته Illuminate\Contracts\Filesystem\Filesystem باشند . لاراول یک رابط ساده و روان برای تعریف این رفتار ارائه می دهد:

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');
});

پیوندهای اولیه

گاهی اوقات ممکن است کلاسی داشته باشید که برخی از کلاس های تزریقی را دریافت می کند، اما به یک مقدار اولیه تزریق شده مانند یک عدد صحیح نیز نیاز دارد. می‌توانید به راحتی از پیوند متنی برای تزریق هر مقداری که کلاس شما به آن نیاز دارد استفاده کنید:

use App\Http\Controllers\UserController;
 
$this->app->when(UserController::class)
->needs('$variableName')
->give($value);

گاهی اوقات یک کلاس ممکن است به آرایه ای از نمونه های برچسب گذاری شده بستگی داشته باشد . با استفاده از این 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 App\Models\Filter;
use App\Services\Logger;
 
class Firewall
{
/**
* The filter instances.
*
* @var array
*/
protected $filters;
 
/**
* Create a new class instance.
*/
public function __construct(
protected Logger $logger,
Filter ...$filters,
) {
$this->filters = $filters;
}
}

با استفاده از اتصال متنی، می توانید این وابستگی را با ارائه give یک بسته به روش که آرایه ای از Filter نمونه های حل شده را برمی گرداند، برطرف کنید:

$this->app->when(Firewall::class)
->needs(Filter::class)
->give(function (Application $app) {
return [
$app->make(NullFilter::class),
$app->make(ProfanityFilter::class),
$app->make(TooLongFilter::class),
];
});

برای راحتی، می‌توانید آرایه‌ای از نام‌های کلاس را نیز ارائه کنید تا هر زمان که به نمونه‌هایی Firewall نیاز داشت ، توسط کانتینر حل شود Filter :

$this->app->when(Firewall::class)
->needs(Filter::class)
->give([
NullFilter::class,
ProfanityFilter::class,
TooLongFilter::class,
]);

وابستگی های تگ متغیر

گاهی اوقات یک کلاس ممکن است وابستگی متغیری داشته باشد که به صورت تایپ به عنوان یک کلاس مشخص ( Report ...$reports ) مشخص می شود. با استفاده از needs و giveTagged متدها، می‌توانید به راحتی تمام اتصالات ظرف را با آن برچسب برای وابستگی داده شده تزریق کنید:

$this->app->when(ReportAggregator::class)
->needs(Report::class)
->giveTagged('reports');

برچسب زدن

گاهی اوقات، ممکن است لازم باشد همه یک «دسته» خاصی از الزام آور را حل کنید. به عنوان مثال، شاید شما در حال ساختن یک تحلیلگر گزارش هستید که آرایه ای از Report پیاده سازی های مختلف رابط را دریافت می کند. پس از ثبت Report پیاده سازی ها، می توانید با استفاده از روش زیر به آنها تگ اختصاص دهید tag :

$this->app->bind(CpuReport::class, function () {
// ...
});
 
$this->app->bind(MemoryReport::class, function () {
// ...
});
 
$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');

هنگامی که سرویس ها برچسب گذاری شدند، می توانید به راحتی همه آنها را از طریق روش کانتینر حل کنید tagged :

$this->app->bind(ReportAnalyzer::class, function (Application $app) {
return new ReportAnalyzer($app->tagged('reports'));
});

گسترش اتصالات

این extend روش امکان اصلاح سرویس های حل شده را می دهد. به عنوان مثال، هنگامی که یک سرویس حل می شود، می توانید کد اضافی را برای تزئین یا پیکربندی سرویس اجرا کنید. این extend متد دو آرگومان را می پذیرد، کلاس سرویسی که در حال گسترش آن هستید و بسته شدنی که باید سرویس تغییر یافته را برگرداند. بسته شدن سرویس در حال حل شدن و نمونه کانتینر را دریافت می کند:

$this->app->extend(Service::class, function (Service $service, Application $app) {
return new DecoratedService($service);
});

حل و فصل

روش make

می توانید از make روش برای حل یک نمونه کلاس از کانتینر استفاده کنید. متد make نام کلاس یا رابطی را که می خواهید حل کنید می پذیرد:

use App\Services\Transistor;
 
$transistor = $this->app->make(Transistor::class);

اگر برخی از وابستگی های کلاس شما از طریق کانتینر قابل حل نیستند، می توانید آنها را با ارسال آنها به عنوان یک آرایه انجمنی به متد تزریق کنید makeWith . برای مثال، ممکن است به صورت دستی $id آرگومان سازنده مورد نیاز Transistor سرویس را ارسال کنیم:

use App\Services\Transistor;
 
$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);

این bound روش ممکن است برای تعیین اینکه آیا یک کلاس یا واسط به صراحت در کانتینر محدود شده است یا خیر استفاده شود:

if ($this->app->bound(Transistor::class)) {
// ...
}

اگر خارج از یک ارائه دهنده خدمات در مکانی از کد خود هستید که به متغیر دسترسی ندارد $app ، می توانید از App نما یا app کمک کننده برای حل یک نمونه کلاس از کانتینر استفاده کنید:

use App\Services\Transistor;
use Illuminate\Support\Facades\App;
 
$transistor = App::make(Transistor::class);
 
$transistor = app(Transistor::class);

Illuminate\Container\Container اگر می‌خواهید خود نمونه کانتینر لاراول به کلاسی که توسط کانتینر حل می‌شود تزریق شود، می‌توانید کلاس را در سازنده کلاس خود تایپ کنید :

use Illuminate\Container\Container;
 
/**
* Create a new class instance.
*/
public function __construct(
protected Container $container
) {}

تزریق خودکار

روش دیگر، و مهمتر از آن، می‌توانید وابستگی را در سازنده کلاسی که توسط کانتینر حل می‌شود، از جمله کنترل‌کننده‌ها ، شنوندگان رویداد ، میان‌افزار و غیره تایپ کنید. علاوه بر این، می‌توانید وابستگی‌هایی را در handle روش مشاغل در صف تایپ کنید . در عمل، این گونه است که بیشتر اشیاء شما باید توسط ظرف حل شوند.

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

<?php
 
namespace App\Http\Controllers;
 
use App\Repositories\UserRepository;
use App\Models\User;
 
class UserController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
protected UserRepository $users,
) {}
 
/**
* Show the user with the given ID.
*/
public function show(string $id): User
{
$user = $this->users->findOrFail($id);
 
return $user;
}
}

روش فراخوانی و تزریق

گاهی اوقات ممکن است بخواهید یک متد را در یک نمونه شیء فراخوانی کنید و در عین حال به ظرف اجازه دهید تا به طور خودکار وابستگی های آن متد را تزریق کند. به عنوان مثال، با توجه به کلاس زیر:

<?php
 
namespace App;
 
use App\Repositories\UserRepository;
 
class UserReport
{
/**
* Generate a new user report.
*/
public function generate(UserRepository $repository): array
{
return [
// ...
];
}
}

می توانید generate روش را از طریق ظرف مانند زیر فراخوانی کنید:

use App\UserReport;
use Illuminate\Support\Facades\App;
 
$report = App::call([new UserReport, 'generate']);

این call روش هر PHP قابل فراخوانی را می پذیرد. روش کانتینر call حتی ممکن است برای فراخوانی یک بسته در حالی که به طور خودکار وابستگی های آن را تزریق می کند استفاده شود:

use App\Repositories\UserRepository;
use Illuminate\Support\Facades\App;
 
$result = App::call(function (UserRepository $repository) {
// ...
});

رویدادهای کانتینری

کانتینر سرویس هر بار که یک شی را حل می کند یک رویداد را شلیک می کند. می توانید با استفاده از resolving روش زیر به این رویداد گوش دهید:

use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) {
// Called when container resolves objects of type "Transistor"...
});
 
$this->app->resolving(function (mixed $object, Application $app) {
// Called when container resolves object of any type...
});

همانطور که می بینید، شی ای که حل می شود به callback ارسال می شود و به شما این امکان را می دهد که هر ویژگی اضافی را روی شی قبل از اینکه به مصرف کننده اش داده شود، تنظیم کنید.

PSR-11

کانتینر سرویس لاراول رابط PSR-11 را پیاده سازی می کند . بنابراین، برای به دست آوردن نمونه ای از کانتینر لاراول، می توانید به رابط کانتینر PSR-11 اشاره کنید:

use App\Services\Transistor;
use Psr\Container\ContainerInterface;
 
Route::get('/', function (ContainerInterface $container) {
$service = $container->get(Transistor::class);
 
// ...
});

اگر شناسه داده شده قابل حل نباشد، یک استثنا ایجاد می شود. Psr\Container\NotFoundExceptionInterface اگر شناسه هرگز محدود نشده باشد، استثنا خواهد بود . اگر شناسه محدود بود اما قابل حل نبود، یک نمونه از Psr\Container\ContainerExceptionInterface پرتاب می شود.