Найти тему
Laravel Topsite Web

Крючки (хуки) жизненного цикла в Laravel

Программисты, должны быть особенно искусны в разбитии больших, сложных проблем на более мелкие. Однако иногда оказывается, что некоторые из тех меньших, повторяющихся битов кода, которые мы извлекли, чтобы уменьшить дублирование (или какую-либо другую потребность), приходится взаимодействовать по-разному в зависимости от некоторого внешнего контекста.

Давайте рассмотрим один из этих сценариев.

У нас есть список полусложных действий, объединенных вместе для выполнения общей большой задачи. Мы берем этот список и объединяем их в серию вызываемых классов. Содержание не важно. Мы перечислим их в нашем классе 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) {
// Resolving the action out of the
// container will help with testing
app($action)();
}
}
}

Это чисто и легко читается. И мы можем использовать этот класс Action Runner как для задания в очереди, так и для команды 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 = 'Запускает список сложных действий.';
public function handle(): int
{
app(ActionRunner::class)->run();
$this->line('Список сложных задач был выполнен успешно!');
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);
}
}
}

Мы начинаем с определения замыкания с возможностью обнуления. Это позволяет нам просто вызвать метод 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}.");
}
}
}

Теперь само по себе это ничего не даст, так как нашему $onprogress присвоено значение 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('Список сложных задач был выполнен успешно!');
return Command::SUCCESS;
}

На данный момент мы могли бы сделать то же самое в запланированном задании, для которого вообще не определен какой-либо хук, или мы могли бы добавить больше каналов уведомлений к заданию, поставленному в очередь.

Вместо этого мы могли бы создать класс ActionContext, который хранил бы много данных и передавал их обратно, позволяя нам действительно выбирать, с какими данными мы хотели бы работать, например, время выполнения, данные модели, другую метаинформацию.

Мы могли бы реализовать несколько различных перехватчиков, которые вызываются в разное время цикла выполнения, например onBeforeExternalApiCalls() или OnCompleted().