Хуки жизненного цикла в Laravel

Источник: «Lifecycle hooks in Laravel - How to build them, and why you'd want to»
Мы, как программисты, должны уметь разбивать большие и сложные задачи на более мелкие и простые в управлении фрагменты. Однако иногда оказывается, что с некоторыми из тех небольших повторяющихся фрагментов кода, которые мы извлекли, чтобы уменьшить дублирование (или по какой-то другой причине), приходится взаимодействовать по-разному в зависимости от некоторого внешнего контекста.

Давайте рассмотрим один из сценариев, и я проведу вас по этому процессу вместе со мной.

Чтобы подготовить сцену, у нас есть список действий средней сложности, связанных вместе для выполнения общей гигантской задачи. Мы берём этот список и превращаем его в серию 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().

Это такой мощный паттерн, который я успешно использовал. И я рад, что смог поделиться им с вами.

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

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

Использование нескольких селекторов с помощью методов селектора JavaScript

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

Шпаргалка по Git