Сервіс-Контейнер
Вступ
The Laravel сервіс-контейнер є потужним інструментом для управління залежностями класів та виконання впровадження залежностей. Впровадження залежностей - це складний термін, який по суті означає наступне: залежності класу "впроваджуються" в клас через конструктор або, в деяких випадках, методи "setter".
Давайте розглянемо простий приклад:
<?php namespace App\Http\Controllers; use App\Services\AppleMusic; use Illuminate\View\View; class PodcastController extends Controller { /** * Створіть новий екземпляр контролера. */ public function __construct( protected AppleMusic $apple, ) {} /** * Показати інформацію про вказаний подкаст. */ public function show(string $id): View { return view('podcasts.show', [ 'podcast' => $this->apple->findPodcast($id) ]); } }
У цьому прикладі PodcastController потребує отримати подкасти з джерела даних, такого як Apple Music. Тому ми інжектуємо сервіс, який здатний отримувати подкасти. Оскільки сервіс інжектується, ми можемо легко "мокати" або створювати фіктивну реалізацію сервісу AppleMusic під час тестування нашого застосунку.
Глибоке розуміння Сервіс-контейнера Laravel є важливим для створення потужного, великого застосунку, а також для внеску в саму ядро Laravel.
Нульова конфігурація резолюції
Якщо клас не має залежностей або залежить лише від інших конкретних класів (не інтерфейсів), контейнеру не потрібно вказувати, як вирішити цей клас. Наприклад, ви можете розмістити наступний код у вашому файлі routes/web.php:
<?php
class Service
{
// ...
}
Route::get('/', function (Service $service) {
dd($service::class);
});
У цьому прикладі, звернення до маршруту вашого застосунку / автоматично вирішить клас Service і впровадить його в обробник вашого маршруту. Це змінює правила гри. Це означає, що ви можете розробляти ваш застосунок і використовувати впровадження залежностей без турбот про роздуті конфігураційні файли.
На щастя, багато класів, які ви будете писати при створенні Laravel застосунку, автоматично отримують свої залежності через контейнер, включаючи контролери, слухачі подій, проміжне програмне забезпечення та інші. Крім того, ви можете вказати залежності в методі handle для завдань у черзі. Як тільки ви відчуєте силу автоматичного та безконфігураційного впровадження залежностей, здається неможливим розробляти без цього.
Коли Використовувати Сервіс-контейнер
Завдяки нульовій конфігурації резолюції, ви часто будете вказувати залежності в маршрутах, контролерах, слухачах подій та в інших місцях без необхідності вручну взаємодіяти з контейнером. Наприклад, ви можете вказати об'єкт Illuminate\Http\Request у визначенні вашого маршруту, щоб легко отримати доступ до поточного запиту. Хоча нам ніколи не доводиться взаємодіяти з контейнером для написання цього коду, він керує ін'єкцією цих залежностей за лаштунками:
use Illuminate\Http\Request;
Route::get('/', function (Request $request) {
// ...
});
У багатьох випадках, завдяки автоматичному впровадженню залежностей та фасадам, ви можете створювати Laravel застосунки без жодного разу ручного зв'язування або вирішення чого-небудь з контейнера. Отже, коли ви взагалі будете взаємодіяти з контейнером вручну? Давайте розглянемо дві ситуації.
По-перше, якщо ви пишете клас, який реалізує інтерфейс, і хочете використати типізацію цього інтерфейсу в маршруті або конструкторі класу, вам потрібно вказати контейнеру, як створювати цей інтерфейс. По-друге, якщо ви розробляєте пакет Laravel, яким плануєте ділитися з іншими розробниками Laravel, можливо, вам потрібно буде зв’язати сервіси вашого пакета з контейнером.
Зв'язування
Основи Зв'язування
Прості Зв'язування
Майже всі ваші зв'язування до сервіс-контейнера будуть зареєстровані в межах сервіс-провайдерів, тому більшість з цих прикладів продемонструють використання контейнера в цьому контексті.
У Сервіс-провайдері ви завжди маєте доступ до контейнера через властивість $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));
});
Note, що ми отримуємо сам контейнер як аргумент для резолвера. Потім ми можемо використовувати контейнер для резолвінгу підзалежностей об'єкта, який ми будуємо.
Як згадувалося, зазвичай ви будете взаємодіяти з контейнером у сервіс-провайдерах; однак, якщо ви хочете взаємодіяти з контейнером поза сервіс-провайдером, ви можете зробити це через App фасад:
use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;
App::bind(Transistor::class, function (Application $app) {
// ...
});
Ви можете використовувати метод bindIf для реєстрації прив'язки контейнера лише в тому випадку, якщо прив'язка ще не була зареєстрована для даного типу:
$this->app->bindIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});
Для зручності, ви можете опустити надання імені класу або інтерфейсу, який ви бажаєте зареєструвати як окремий аргумент, і натомість дозволити Laravel визначити тип з типу, що повертається замиканням, яке ви надаєте методу bind:
App::bind(function (Application $app): Transistor {
return new Transistor($app->make(PodcastParser::class));
});
There is no need to bind classes into the container if they do not depend on any interfaces. Контейнер не потребує інструкцій щодо створення цих об'єктів, оскільки він може автоматично вирішувати ці об'єкти за допомогою рефлексії.
Зв'язування Singleton
Метод 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 Singleton'ів
Метод scoped зв'язує клас або інтерфейс у контейнері, який повинен бути вирішений лише один раз протягом певного життєвого циклу запиту / завдання Laravel. Хоча цей метод схожий на метод singleton, екземпляри, зареєстровані за допомогою методу scoped, будуть очищені щоразу, коли застосунок Laravel починає новий "життєвий цикл", наприклад, коли Laravel Octane обробляє новий запит або коли працівник черги Laravel обробляє нове завдання:
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));
});
Ви можете використовувати метод scopedIf для реєстрації обмеженого зв'язування контейнера лише якщо зв'язування ще не було зареєстровано для даного типу:
$this->app->scopedIf(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 у конструкторі класу, який вирішується контейнером. Пам'ятайте, контролери, слухачі подій, проміжне програмне забезпечення та різні інші типи класів у Laravel застосунках завжди вирішуються за допомогою контейнера:
use App\Contracts\EventPusher;
/**
* Створити новий екземпляр класу.
*/
public function __construct(
protected EventPusher $pusher,
) {}
Контекстне Зв'язування
Іноді у вас може бути два класи, які використовують один і той самий інтерфейс, але ви бажаєте впровадити різні реалізації в кожен клас. Наприклад, два контролери можуть залежати від різних реалізацій Illuminate\Contracts\Filesystem\Filesystem контракту. Laravel надає простий, зрозумілий інтерфейс для визначення цієї поведінки:
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');
});
Контекстні Атрибути
Оскільки контекстне зв'язування часто використовується для впровадження реалізацій драйверів або значень конфігурації, Laravel пропонує різноманітні атрибути контекстного зв'язування, які дозволяють впроваджувати такі типи значень без ручного визначення контекстних зв'язувань у ваших Сервіс-провайдерах.
Наприклад, атрибут Storage може бути використаний для впровадження конкретного диска зберігання:
<?php
namespace App\Http\Controllers;
use Illuminate\Container\Attributes\Storage;
use Illuminate\Contracts\Filesystem\Filesystem;
class PhotoController extends Controller
{
public function __construct(
#[Storage('local')] protected Filesystem $filesystem
) {
// ...
}
}
На додаток до атрибута Storage, Laravel пропонує атрибути Auth, Cache, Config, Context, DB, Give, Log, RouteParameter та Tag:
<?php
namespace App\Http\Controllers;
use App\Contracts\UserRepository;
use App\Models\Photo;
use App\Repositories\DatabaseRepository;
use Illuminate\Container\Attributes\Auth;
use Illuminate\Container\Attributes\Cache;
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\Context;
use Illuminate\Container\Attributes\DB;
use Illuminate\Container\Attributes\Give;
use Illuminate\Container\Attributes\Log;
use Illuminate\Container\Attributes\RouteParameter;
use Illuminate\Container\Attributes\Tag;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Database\Connection;
use Psr\Log\LoggerInterface;
class PhotoController extends Controller
{
public function __construct(
#[Auth('web')] protected Guard $auth,
#[Cache('redis')] protected Repository $cache,
#[Config('app.timezone')] protected string $timezone,
#[Context('uuid')] protected string $uuid,
#[Context('ulid', hidden: true)] protected string $ulid,
#[DB('mysql')] protected Connection $connection,
#[Give(DatabaseRepository::class)] protected UserRepository $users,
#[Log('daily')] protected LoggerInterface $log,
#[RouteParameter('photo')] protected Photo $photo,
#[Tag('reports')] protected iterable $reports,
) {
// ...
}
}
Більше того, Laravel надає атрибут CurrentUser для впровадження поточного автентифікованого користувача в заданий маршрут або клас:
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
Route::get('/user', function (#[CurrentUser] User $user) {
return $user;
})->middleware('auth');
Визначення Користувацьких Атрибутів
Ви можете створити власні контекстуальні атрибути, реалізуючи контракт Illuminate\Contracts\Container\ContextualAttribute. Контейнер викликатиме метод resolve вашого атрибута, який повинен вирішити значення, яке має бути впроваджене в клас, що використовує атрибут. У наведеному нижче прикладі ми повторно реалізуємо вбудований атрибут Laravel Config:
<?php namespace App\Attributes; use Attribute; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\ContextualAttribute; #[Attribute(Attribute::TARGET_PARAMETER)] class Config implements ContextualAttribute { /** * Створіть новий екземпляр атрибута. */ public function __construct(public string $key, public mixed $default = null) { } /** * Отримати значення конфігурації. * * @param self $attribute * @param \Illuminate\Contracts\Container\Container $container * @return mixed */ public static function resolve(self $attribute, Container $container) { return $container->make('config')->get($attribute->key, $attribute->default); } }
Зв'язування Примітивів
Іноді у вас може бути клас, який отримує деякі ін'єкційні класи, але також потребує ін'єкційного примітивного значення, такого як ціле число. Ви можете легко використовувати контекстне зв'язування, щоб ін'єктувати будь-яке значення, яке може знадобитися вашому класу:
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');
Зв’язування типізованих варіативних параметрів
Іноді у вас може бути клас, який отримує масив типізованих об'єктів, використовуючи варіативний аргумент конструктора:
<?php use App\Models\Filter; use App\Services\Logger; class Firewall { /** * Екземпляри фільтрів. * * @var array */ protected $filters; /** * Створіть новий екземпляр класу. */ 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);
Якщо ви хочете, щоб екземпляр контейнера Laravel був впроваджений у клас, який вирішується контейнером, ви можете вказати клас Illuminate\Container\Container у конструкторі вашого класу:
use Illuminate\Container\Container; /** * Створіть новий екземпляр класу. */ public function __construct( protected Container $container, ) {}
Автоматичне Ін'єктування
Альтернативно, і що важливо, ви можете вказати залежність у конструкторі класу, який вирішується контейнером, включаючи контролери, слухачі подій, проміжне програмне забезпечення та інше. Крім того, ви можете вказати залежності в методі handle для завдань у черзі. На практиці, саме так більшість ваших об'єктів повинні вирішуватися контейнером.
Наприклад, ви можете вказати тип сервісу, визначеного вашим застосунком, у конструкторі контролера. Сервіс буде автоматично вирішено та впроваджено в клас:
<?php namespace App\Http\Controllers; use App\Services\AppleMusic; class PodcastController extends Controller { /** * Створіть новий екземпляр контролера. */ public function __construct( protected AppleMusic $apple, ) {} /** * Показати інформацію про вказаний подкаст. */ public function show(string $id): Podcast { return $this->apple->findPodcast($id); } }
Виклик Методу та Ін'єкція
Іноді ви можете захотіти викликати метод на екземплярі об'єкта, дозволяючи контейнеру автоматично впроваджувати залежності цього методу. Наприклад, враховуючи наступний клас:
<?php namespace App; use App\Services\AppleMusic; class PodcastStats { /** * Згенерувати новий звіт статистики подкасту. */ public function generate(AppleMusic $apple): array { return [ // ... ]; } }
Ви можете викликати метод generate через контейнер наступним чином:
use App\PodcastStats;
use Illuminate\Support\Facades\App;
$stats = App::call([new PodcastStats, 'generate']);
Метод call приймає будь-який виклик, допустимий у PHP. Метод call контейнера можна навіть використати для виклику замикання з автоматичним упровадженням його залежностей:
use App\Services\AppleMusic;
use Illuminate\Support\Facades\App;
$result = App::call(function (AppleMusic $apple) {
// ...
});
Події контейнера
Сервісний контейнер викликає подію щоразу, коли створює об'єкт. Ви можете прослуховувати цю подію за допомогою методу resolving:
use App\Services\Transistor; use Illuminate\Contracts\Foundation\Application; $this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) { // Викликається, коли контейнер створює об'єкти типу "Transistor"... }); $this->app->resolving(function (mixed $object, Application $app) { // Викликається, коли контейнер створює об'єкт будь-якого типу... });
Як бачите, об'єкт, що створюється, буде передано зворотному виклику, що дозволяє встановити будь-які додаткові властивості об'єкта перед тим, як він буде переданий споживачу.
Повторне зв’язування
Метод rebinding дозволяє прослуховувати момент повторного зв’язування сервісу з контейнером, тобто коли він реєструється повторно або перевизначається після початкового зв’язування. Це може бути корисно, коли потрібно оновити залежності або змінити поведінку щоразу, коли оновлюється певне зв’язування:
use App\Contracts\PodcastPublisher; use App\Services\SpotifyPublisher; use App\Services\TransistorPublisher; use Illuminate\Contracts\Foundation\Application; $this->app->bind(PodcastPublisher::class, SpotifyPublisher::class); $this->app->rebinding( PodcastPublisher::class, function (Application $app, PodcastPublisher $newInstance) { // }, ); // Нове зв’язування викличе замикання rebinding... $this->app->bind(PodcastPublisher::class, TransistorPublisher::class);
PSR-11
Сервіс-контейнер Laravel реалізує інтерфейс PSR-11. Тому ви можете використовувати type-hint інтерфейсу контейнера PSR-11, щоб отримати екземпляр контейнера Laravel:
use App\Services\Transistor;
use Psr\Container\ContainerInterface;
Route::get('/', function (ContainerInterface $container) {
$service = $container->get(Transistor::class);
// ...
});
Виняток буде викинуто, якщо даний ідентифікатор не може бути вирішено. Виняток буде екземпляром Psr\Container\NotFoundExceptionInterface, якщо ідентифікатор ніколи не був зв'язаний. Якщо ідентифікатор був зв'язаний, але не міг бути вирішений, буде викинуто екземпляр Psr\Container\ContainerExceptionInterface.
