在laravel项目中创建Lifecycle hooks(生命周期钩子)及使用钩子的场景浅析

2023-06-01 00:00:00 钩子 生命周期 浅析

作为程序员, 必须特别善于把大型的, 复杂的问题分解成较小的, 易于管理的块. 

然而,有时会发现,我们为了减少重复(或其他需要)而提取的那些较小的、重复的代码片段,必须根据一些外部环境以不同的方式进行交互。


让我们来探索其中的一个场景,我将引导你和我一起完成这个过程。

为了设定场景,我们有一个半复杂的动作列表,将其串联起来执行一个整体的巨大任务。

我们把这个列表做成一系列的可调用类,内容并不重要。

我们将在我们的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()。


我认为这是一个非常强大的模式。

相关文章