在laravel项目中创建Lifecycle hooks(生命周期钩子)及使用钩子的场景浅析
作为程序员, 必须特别善于把大型的, 复杂的问题分解成较小的, 易于管理的块.
然而,有时会发现,我们为了减少重复(或其他需要)而提取的那些较小的、重复的代码片段,必须根据一些外部环境以不同的方式进行交互。
让我们来探索其中的一个场景,我将引导你和我一起完成这个过程。
为了设定场景,我们有一个半复杂的动作列表,将其串联起来执行一个整体的巨大任务。
我们把这个列表做成一系列的可调用类,内容并不重要。
我们将在我们的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)();
}
}
}
这很容易阅读。
而且我们可以在队列作业和artisan命令中使用这一个ActionRunner。
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);
}
}
}
在这个trait中,我们首先定义了一个nullable Closure。
这使得我们可以简单地调用 callOnProgressHook() 方法,并让 trait 担心我们是否要对它做什么。
如果我们没有用流畅的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()。
我认为这是一个非常强大的模式。
相关文章