Инъекция зависимостей в командах Laravel Artisan

Источник: «Laravel Artisan Command Dependency Injection»
Использование метода __construct() для инъекции зависимостей в командах Laravel Artisan может привести к неожиданным последствиям.

tl;dr Не внедряйте зависимости для команд Artisan с помощью метода __construct(), используйте handle().

Under Construction

Сервис-контейнер Laravel — это волшебная вещь. Он позволяет нам разрешать зависимости из воздуха, не заботясь о том, как эти зависимости построены. В сочетании с продвижением свойств конструктора, которое было добавлено в PHP в 8-й версии, всё, что нам нужно сделать, это:

public function __construct(
private readonly FooRepository $fooRepository,
private readonly BarService $barService,
private readonly BazInterface $bazInterface,
) {
}

Необходимо убедиться, что нет никаких круговых зависимостей, но в остальном это то, что мы можем "установить и забыть". Однако есть одно место, где это может привести к непредвиденным последствиям: Команды Artisan.

I Command You

Если вы используете php artisan make:command для создания новой команды, Laravel добавляет метод __construct(), готовый и ожидающий, пока вы внедрите все классы, на которые опирается команда.

use Illuminate\Console\Command;

class TestCommand extends Command
{
...

/**
* Create a new command instance.
*
* @return void
*/

public function __construct()
{
parent::__construct();
}

...
}

В большинстве случаев совершенно ясно, где вызываются эти конструкторы, но в командах Artisan это может происходить чаще, чем вы думаете. Чтобы продемонстрировать это, давайте добавим нашего доброго друга dd() в метод __construct() нашей команды.

use Illuminate\Console\Command;

class TestCommand extends Command
{
...

/**
* Create a new command instance.
*
* @return void
*/

public function __construct()
{
parent::__construct();

dd('Boom!');
}

...
}

Если мы вызовем нашу новую команду, используя php artisan app:test-command, то результат не удивит:

php artisan app:test-command
"Boom!" // app/Console/Commands/TestCommand.php:34

Однако, когда мы отдалимся от этой команды, всё становится немного неожиданнее:

php artisan list
"Boom!" // app/Console/Commands/TestCommand.php:34

Эта команда выводит список всех доступных команд Artisan. Список включает сигнатуру и описание нашей тестовой команды, так что, возможно, всё не так уж неожиданно. Как насчёт этого:

php artisan migrate
"Boom!" // app/Console/Commands/TestCommand.php:34

Миграция базы данных, кажется, не должна иметь никакого отношения к нашей новой команде.

Всякий раз, когда вы запускаете команду, начинающуюся с php artisan, загружается ядро консоли. При этом создаётся экземпляр каждой команды в нашем приложении. А что происходит, когда мы создаём экземпляр каждой команды? Благодаря сервис-контейнеру, он также разрешает экземпляр каждой из их зависимостей. Большинство этих зависимостей можно быстро разрешить, но по мере роста проекта мы можем столкнуться с сотнями или даже тысячами команд.

Я знаю, что вы можете подумать: Я не так часто выполняю эти команды. Не забывайте, что это также повлияет:

Консоль Artisan — это быстрый способ запуска кода приложения, и, как и в других областях нашего приложения, мы хотели бы, чтобы он оставался таким! К счастью, есть простое решение: используйте handle().

How To Handle() Dependencies

Основная функциональность команды Artisan содержится в методе handle(). Это метод, вызываемый при выполнении команды. Метод handle() также поддерживает инъекцию зависимостей, но не вызывается, когда мы запускаем что-либо, кроме этой команды.

Один из недостатков использования метода handle() для инъекции зависимостей заключается в том, что мы больше не можем использовать продвижение свойств конструктора или помечать зависимости как доступные только для чтения. Поэтому нам придётся вернуться к старому доброму методу объявления свойств:

use Illuminate\Console\Command;

class TestCommand extends Command
{
...

private FooRepository $fooRepository;

private BarService $barService;

private BazInterface $bazInterface;

/**
* Execute the console command.
*
* @return int
*/

public function handle(
FooRepository $fooRepository,
BarService $barService,
BazInterface $bazInterface,
) {
$this->fooRepository = $fooRepository;
$this->barService = $barService;
$this->bazInterface = $bazInterface;

return 0;
}
}

Некоторые из вас, возможно, содрогнутся от возвращения к этим пещерным методам. Для нас важнее, чтобы команды Artisan были лёгкими и быстрыми. Расскажите о ваших соображениях по поводу компромиссов между этими двумя подходами. Спасибо за прочтение!

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

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

Введение в CSRF-токены в Symfony

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

Модернизация конфигурации Symfony