Инъекция зависимостей в командах Laravel Artisan
__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
, загружается ядро консоли. При этом создаётся экземпляр каждой команды в нашем приложении. А что происходит, когда мы создаём экземпляр каждой команды? Благодаря сервис-контейнеру, он также разрешает экземпляр каждой из их зависимостей. Большинство этих зависимостей можно быстро разрешить, но по мере роста проекта мы можем столкнуться с сотнями или даже тысячами команд.
Я знаю, что вы можете подумать: Я не так часто выполняю эти команды
. Не забывайте, что это также повлияет:
php artisan schedule:run
— вызывается каждую минуту, если вы определили её вcrontab
.php artisan queue:work
— каждый раз, когда вы запускаете worker очереди и каждый раз, когда они перезапускаются с помощью--max-jobs
или--max-time
.php artisan serve
— каждый раз, когда вы запускаете локальный сервер разработки.
Консоль 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 были лёгкими и быстрыми. Расскажите о ваших соображениях по поводу компромиссов между этими двумя подходами. Спасибо за прочтение!