Command Bus (командная шина)
Введение
Командная шина в Laravel - это удобный способ инкапсуляции (изолирования) задач вашего приложения в простые и понятные «команды». Чтобы разобраться в назначении команд, давайте представим, что мы пишем приложение, в котором мы будем продавать подкасты нашим пользователям.
Когда пользователь приобретает подкаст, нам нужно произвести несколько действий: мы должны снять деньги с его карты, добавить запись в БД, что покупка успешно состоялась и отослать email с подтверждением покупки. Возможно, еще нам нужно провести несколько проверок - а разрешено ли этому пользователю вообще покупать этот конкретный подкаст.
Обычно мы размещаем всю эту логику в контроллере, однако такой подход имеет несколько недостатков. Во-первых, класс контроллера обычно содержит методы для обработки нескольких HTTP-запросов, и располагая логику в каждом методе мы делаем контроллеры излишне большими, сложно читаемыми и сложно поддерживаемыми. Во-вторых, такой код трудно использовать вне контекста контроллера и HTTP-запроса (например, если мы хотим выполнять покупку подкастов из командной строки или очереди). В-третьих, это затрудняет тестирование, так как мы вынуждены эмулировать HTTP-запрос и разбирать HTTP-ответ.
Поэтому вместо помещения этой логики в контроллер мы оформим её в так называемую команду и назовём её PurchasePodcast
.
Создание команды
Создаем команду при помощи artisan-команды make:command
:
php artisan make:command PurchasePodcast
Созданный этой командой класс помещается в папку app/Commands
. По умолчанию команда содержит два метода - конструктор и метод handle
.
При помощи конструктора вы можете добавить нужные зависимости в класс команды, а метод handle
собственно исполняет команду. Например:
class PurchasePodcast extends Command implements SelfHandling {
protected $user, $podcast;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(User $user, Podcast $podcast)
{
$this->user = $user;
$this->podcast = $podcast;
}
/**
* Execute the command.
*
* @return void
*/
public function handle()
{
// Пишем функцонал покупки подкаста здесь...
event(new PodcastWasPurchased($this->user, $this->podcast));
}
}
Метод handle
тоже может принимать зависимости в аргументах (type hinting), как и конструктор. Как и конструктору, они будут поданы на вход
автоматически при помощи IoC контейнера.
/**
* Execute the command.
*
* @return void
*/
public function handle(BillingGateway $billing)
{
// Пишем функцонал покупки подкаста здесь...
}
Выполнение команд
Мы создали команду, как теперь запустить её? Конечно, мы можем выполнить метод handle
нашего класса, однако лучше запускать его через
командную шину Laravel. О преимуществах такого подхода будет рассказано ниже.
Если вы взглянете на базовый контроллер вашего приложения, который расширяют ваши собственные контроллеры, вы увидите там трейт DispatchesCommands
.
Этот трейт позволяет запускать команды при помощи метода dispatch
. Например:
public function purchasePodcast($podcastId)
{
$this->dispatch(
new PurchasePodcast(Auth::user(), Podcast::findOrFail($podcastId))
);
}
Командная шина исполняет команду и берёт на себя всю рутину по обеспечению класса команды всеми необходимыми зависимостями, перечислеными
в аргументах конструктора класса команды и аргументах метода handle
.
Вы можете задействовать командную шину в любом вашем классе - для этого добавьте трейт Illuminate\Foundation\Bus\DispatchesCommands
.
Если вы хотите принимать инстанс командной шины в конструкторе, то укажите в аргументах (type hint) объект Illuminate\Contracts\Bus\Dispatcher
.
И наконец вы можете просто использовать фасад Bus
для запуска команды:
Bus::dispatch(
new PurchasePodcast(Auth::user(), Podcast::findOrFail($podcastId))
);
Передача аргументов из запроса
Практически всегда вам понадобится передать данные HTTP-запроса (например, выборочное из $_POST) в команду. Вместо того, чтобы заставлять вас
вручную описывать каждый запрос, Laravel предлагает нечто более автоматизированное. Посмотрите на метод dispatchFrom
трейта DispatchesCommands
:
$this->dispatchFrom('Command\Class\Name', $request);
Этот метод смотрит конструктор класса, имя которого передано первым аргументом, и вынимает из переменной $request (это HTTP-запрос или просто
любой объект типа ArrayObject
) те ключи, которые совпадают с названием переменных в аргументе конструктора.
Например, если в аргументах конструктора есть переменная $firstNаme
, то ей присвоится значение firstName
HTTP-запроса.
Вы можете передать третьим аргументом массив значений по умолчанию:
$this->dispatchFrom('Command\Class\Name', $request, [
'firstName' => 'Taylor',
]);
Запуск команды в фоне
Командная шина может применяться не только для немедленного запуска задач в текущем запросе, но и помещать команды в очередь для того, чтобы запустить их в отдельном процессе. Таким образом, командная шина может быть основным инструментом для работы с очередями.
Для создания команды, которая будет запускаться в фоне, добавьте флаг --queued
:
php artisan make:command PurchasePodcast --queued
Созданный класс будет наследовать интерфейс Illuminate\Contracts\Queue\ShouldBeQueued
и иметь трейт SerializesModels
.
Этот функционал позволит команде добавляться в очередь для последующего запуска слушателем очереди, а также добавит возможность сериализовать
и десериализовать Eloquent-модели.
Если у вас уже есть созданная команда и вы хотите сделать её работающей в фоне, просто вручную добавьте
implements Illuminate\Contracts\Queue\ShouldBeQueued
. Этот интерфейс не содержит обязательных методов и является просто индикатором
для командной шины. После этого метод dispatch
вместо того, чтобы запустить команду, поместит её в очередь для последующего запуска в фоне.
Чтобы узнать поподробнее о том, как в Laravel осуществляется запуск задач в фоне, обратитесь к документации по очередям.
Конвейеры команд
До того, как команда будет передена диспетчеру, вы можете предать её по конвейеру другим классам. Конвейеры команд работают также, как и HTTP-посредники. Например, можно завернуть все операции, выполняемые командой, в транзакцию или просто записать в лог факт её выполнения.
Для добавления конвейера к вашей командной шине, вызовите метод pipeThrough
диспетчера в своём методе App\Providers\BusServiceProvider::boot
:
$dispatcher->pipeThrough(['UseDatabaseTransactions', 'LogCommand']);
Конвейер команд описывается в методе handle
:
class UseDatabaseTransactions {
public function handle($command, $next)
{
return DB::transaction(function() use ($command, $next)
{
return $next($command);
}
}
}
Классы, которые необходимо задействовать в конвейере, ищутся и загружаются через сервис-контейнер, поэтому вы можете указывать любые зависимости в их конструкторах.
В качестве элемента конвейера так же можно использовать замыкания:
$dispatcher->pipeThrough([function($command, $next)
{
return DB::transaction(function() use ($command, $next)
{
return $next($command);
}
}]);