Laravel: Всё о контейнере внедрения зависимостей

Источник: «Leaning on the container»
В Laravel есть фантастический контейнер внедрения зависимостей, но многие избегают его. В этой статье я расскажу, как использовать контейнер 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 в своих интересах, имея небольшие движущиеся части, которые объединяются для достижения конечной цели.

Дополнительные материалы

Предыдущая Статья

Laravel: Как обрабатывать длительные задания

Следующая Статья

Laravel: DDD и Объект-Значение