Laravel: Всё о контейнере внедрения зависимостей
Использование контейнера заключается в организованности — место для постоянного хранения привязок (bindings) контейнера и соглашений об именах, которые имеют смысл и позволяют знать, что происходит. В конце концов, контейнер настолько хорош, насколько хорошо владелец его использует.
Допустим мы хотим хранить все привязки в одном месте, в сервис провайдере. Звучит разумно, верно? Но что происходит по мере роста нашего приложения? Для простого приложения мы начнём, может быть, с 5-6 привязок, добавим несколько новых функций и нужно добавить ещё привязки к контейнеру. Через некоторое время используемый сервис провайдер, станет чрезвычайно большим и будет требоваться много когнитивных усилий, чтобы найти что-либо.
Как с этим бороться? Как гарантировать, что мы не просто запихиваем их в сервис провайдер, чтобы скрыть проблемы? Позвольте рассказать вам, как я подхожу к этому вопросу.
Мой основной AppServiceProvider
— точка входа в моё приложение, поэтому моя задача — зарегистрировать его ключевые области. Я опираюсь на DDD, поэтому у меня один сервис провайдер для каждого домена. Я разрешаю каждому домену прозрачно управлять своими привязками.
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(
provider: AuthDomainServiceProvider::class,
);
$this->app->register(
provider: CommunicationDomainServiceProvider::class,
);
$this->app->register(
provider: WorkDomainServiceProvider::class,
);
}
}
Используя этот подход, я могу включать и отключать домены по мере необходимости, быстро добавлять новый и получать обзор всех доменов в моём приложении из одного файла.
Конечно, я оставляю другие сервис провайдеры идущие по умолчанию с приложением Laravel, поскольку у них есть своё предназначение. Итак, давайте рассмотрим один из сервис провайдеров домена, чтобы понять, для чего он используется:
final class AuthDomainServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(
provider: QueryServiceProvider::class,
);
$this->app->register(
provider: CommandServiceProvider::class,
);
$this->app->register(
provider: FactoryServiceProvider::class,
);
}
}
Таким образом, мой сервис провайдер аутентификации используется исключительно для регистрации аспектов домена, которым необходимо написать свои привязки.
Запрос/Query — это операции чтения в приложении, общие запросы или части запросов которые мне нужно выполнить. Я написал об этом статью Laravel: Эффективный Eloquent.
Команда/Command - операции записи в приложении. Обычно включаются в фоновые задания, чтобы приложение могло быстро реагировать.
Фабрика/Factory - фабрики объектов данных. Я обнаружил, что объекты данных становятся большими, беспорядочными и занимают много места. Моё решение состояло в перемещение их в выделенные фабрики, которые я могу использовать для создания объектов данных в своём приложении.
Давайте посмотрим на CommandServiceProvider
и на то, как его можно использовать для эффективной регистрации команд в нашем приложении.
final class CommandServiceProvider extends ServiceProvider
{
public array $bindings = [
FindOrCreateUserContract::class => FindOrCreateUser::class,
GenerateApiTokenContract::class => GenerateApiToken::class,
SendPasswordResetContract::class => SendPasswordReset::class,
];
}
Laravel позволяет использовать свойство bindings
на сервис провайдере для регистрации любых привязок, которым не нужно передавать аргументы. Это поддерживает чистоту в наших сервис провайдерах.
Давайте посмотрим на одну из этих привязок, чтобы понять, как они выглядят и для чего используются.
interface GenerateApiTokenContract
{
public function handle(Authenticatable $user, DataObjectContract $payload): Model|NewAccessToken;
}
Затем перейдём к реализации.
final class GenerateApiToken implements GenerateApiTokenContract
{
public function handle(Authenticatable $user, DataObjectContract $payload): Model|NewAccessToken
{
return DB::transaction(
fn (): Model|NewAccessToken => $user->createToken(
name: $payload->name,
),
);
}
}
Мы заключаем операцию записи в транзакцию базы данных, затем используем внедрённую пользовательскую модель и вызываем для неё метод создания токена createToken
, передавая свойство name
из нашей полезной нагрузки $payload
. Это сохраняет чистоту — так как тогда вы также можете использовать её для генерации API токенов для любого пользователя в вашем приложении, а не только для текущего вошедшего в систему.
Использование контейнера таким образом означает, что контроллеры всегда чисты и минимальны. Давайте рассмотрим пример API контроллера для входа пользователя.
final readonly class LoginController
{
public function __construct(
private GenerateApiTokenContract $command,
private TokenNameGenerator $generator,
) {}
public function __invoke(LoginRequest $request): Responsable
{
$request->authenticate();
return new TokenResponse(
data: TokenFactory::make(
data: $this->command->handle(
user: auth()->user(),
payload: new TokenRequest(
name: $generator->generate(),
),
),
),
);
}
}
Конечно, вы можете разделить этот код, дав ему больше пространства. Но это не то, к чему я стремлюсь. Я опираюсь на контейнер и использую Laravel в своих интересах, имея небольшие движущиеся части, которые объединяются для достижения конечной цели.