Хуки жизненного цикла в Laravel
Давайте рассмотрим один из сценариев, и я проведу вас по этому процессу вместе со мной.
Чтобы подготовить сцену, у нас есть список действий средней сложности, связанных вместе для выполнения общей гигантской задачи. Мы берём этот список и превращаем его в серию invokable
классов. Содержание неважно. Мы перечислим их в классе ActionRunner
. Затем перебираем их и выполняем каждый по порядку из метода run()
.
namespace App;
use App\Actions;
class ActionRunner
{
public array $actions = [
Actions\FirstStepOfComplexThing::class,
Actions\SecondStepOfComplexThing::class,
Actions\ThirdStepOfComplexThing::class,
Actions\FourthStepOfComplexThing::class,
Actions\FifthStepOfComplexThing::class,
Actions\SixthStepOfComplexThing::class,
];
public function run(): void
{
foreach ($this->actions as $action) {
// Разрешение экшенов вне контейнера
// поможет при тестировании
app($action)();
}
}
}
Это хорошо. Это чисто и легко читается. И мы можем использовать этот ActionRunner
как в задании в очереди, так и в команде artisan
.
namespace App\Jobs;
use App\ActionRunner;
class RunActionsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public function handle(): void
{
app(ActionRunner::class)->run();
}
}
namespace App\Commands;
use App\ActionRunner;
class RunActionsCommand extends Command
{
protected $signature = 'complicated-thing:run';
protected $description = 'Runs the list of complicated actions.';
public function handle(): int
{
app(ActionRunner::class)->run();
$this->line('The list of complicated things was run successfully!');
return Command::SUCCESS;
}
}
Однако здесь мы можем столкнуться с проблемой. Скажем, это какие-то особо тяжёлые действия, и каждое из них может выполняться около минуты. Этот рабочий процесс займёт 5 минут без обратной связи. Теперь предположим, что когда эти действия выполняются в очереди, мы хотели бы отправить сообщение в канал Slack после завершения каждого действия. Но при запуске команды мы хотим пропустить это и вывести в CLI.
Инстинкт может подсказать: Мы просто скопируем метод
. И да, это сработает. Но теперь мы не только дублируем код (что не всегда плохо), но и эти классы берут на себя ответственность, которая им не нужна. От них требуется знание, которого на самом деле не должно быть, и если мы изменим принцип работы этой функции, нам придётся изменить её в нескольких местах.run()
в задание и команду и сделаем там цикл со всем, что нужно сделать
Я собираюсь показать подход, который мне очень нравится. Мы собираемся реализовать хук OnProgress()
. Поскольку это хук, скорее всего, можно использовать и в других местах этого фейкового приложения, мы продолжим и просто сделаем его трейтом.
namespace App\Traits;
use Closure;
trait UsesOnProgressHook
{
public ?Closure $onProgressFn = null;
public function onProgress(Closure $fn): self
{
$this->onProgressFn = $fn;
return $this;
}
public function callOnProgressHook(...$args): void
{
if ($this->onProgressFn) {
($this->onProgressFn)(...$args);
}
}
}
В этом трейте мы начинаем с определения nullable
Closure
. Это позволяет просто вызывать метод callOnProgressHook()
и позволить трейту беспокоиться о том, будем ли мы что-то с ним делать. Если мы не установили функцию с плавным методом onProgress()
, он становится неактивным. Теперь ActionRunner
становится примерно таким:
namespace App
use App\Actions;
use App\Traits\UsesOnProgressHook;
class ActionRunner
{
use UsesOnProgressHook;
public array $actions = [
Actions\FirstStepOfComplexThing::class,
Actions\SecondStepOfComplexThing::class,
Actions\ThirdStepOfComplexThing::class,
Actions\FourthStepOfComplexThing::class,
Actions\FifthStepOfComplexThing::class,
Actions\SixthStepOfComplexThing::class,
];
public function run(): void
{
foreach ($this->actions as $action) {
app($action)();
$this->callOnProgressHook("Ran {$action}.");
}
}
}
Само по себе это ничего не даст, так как для $onProgressFn
установлено значение null
. Но именно здесь проявляется безграничная космическая сила этого хука.
Мы можем немного изменить метод handle()
задания очереди и разрешить ему отправлять это сообщение в канал Slack:
public function handle(): void
{
app(ActionRunner::class)
->onProgress(fn (string $progress) => Log::channel('slack')->info($progress))
->run();
}
Точно так же мы можем изменить метод handle()
нашей команды и вместо этого позволить ему выводить в консоль:
public function handle(): int
{
app(ActionRunner::class)
->onProgress(fn (string $progress) => $this->info($progress))
->run();
$this->line('The list of complicated things was run successfully!');
return Command::SUCCESS;
}
На этом этапе мы могли бы сделать то же самое в запланированном задании, для которого вообще не определён ни один хук, или мы могли бы добавить больше каналов уведомлений в задание в очереди. Вместо этого мы могли бы создать класс ActionContext
, который хранил бы тонны данных и передал бы их обратно, позволяя нам выбирать, с какими данными мы хотели бы работать. Например, время выполнения, данные модели, другую метаинформацию и т.д. Мы могли бы реализовать несколько разных хуков, которые вызываются в разное время в цикле выполнения, например, onBeforeExternalApiCalls()
или onCompleted()
.
Это такой мощный паттерн, который я успешно использовал. И я рад, что смог поделиться им с вами.