在Laravel中优化Actions的限制和不足

2023-06-01 00:00:00 laravel 优化 Actions

在过去的一年多时间里, 基于Action(行动)的方法在Laravel世界中越来越受欢迎. 

我是这个方法的忠实粉丝,并在较早的时候就采用了它。

然而, 随着时间的推移, 我发现我把所有可以想象到的东西都放在Actions命名空间下, 而目录也越来越拥挤. 

我从试图简化我的过程到把所有东西都提取到动作中--而每次找到正确的动作都变得越来越复杂。

我决定做点什么,找到一种方法,让我仍能从这种方法中受益,但又不至于把所有东西都归类为行动。


由于以前使用过各种架构模式,我回想了一些对我很有效的模式。

我是CQRS(Command Query Responsibility Segregation)的忠实粉丝。

然而,我不喜欢手动添加我所有的映射的方法,这样我就可以靠在命令总线或查询总线上。

但是我可以采取我喜欢的这种模式,靠着Laravels容器来做我需要的事情。


所以我决定把我归类为Actions的东西拆成更接近CQRS的方法。

命令是我想要执行的写动作,而查询是我的读动作。让我们看一下这些变化的例子。

namespace App\Actions;
 
final class CreateNewUserAction
{
    public function handle(NewUser $user): Model|User
    {
        return DB::transaction(
            callback: static fn () => User::query()->create($user->toArray()),
            attempts: 2,
        );
    }
}

这是一个典型的写动作的例子。

它将接受一个DTO作为其有效载荷,并在数据库事务中执行一个写入查询。

这对我来说很有效,但作为一个命令,它看起来像什么呢?

namespace App\Commands\Users;
 
final class CreateNewUser
{
    public function handle(NewUser $user): Model|User
    {
        return DB::transaction(
            callback: static fn () => User::query()->create($user->toArray()),
            attempts: 2,
        );
    }
}

这是同样的事情,但在不同的命名空间下,所以我的代码中有更好的分离。

你可以用类似的方法来处理动作,并创建嵌套的命名空间--但你不会得到与你分割读写操作时一样的意图分离。

接下来让我举一个更复杂的例子。


如果你看过我的Laravel业务流程建模教程,你就会明白这个过程。

让我们假设我们有一个流程,我们想为我们的用户创建一个新的团队。

我们为此采取的步骤如下:

1.创建一个新的团队模型.
2.将我们的用户作为一个团队成员。
3.给我们的用户发电子邮件,通知他们。
4.设置一个团队所需的任何额外资源,也许是计费。

如果你在控制器中看,这是一个很大的问题,如果你使用动作,甚至命令和查询--你最终会有很多东西被拉到一个地方,命名变得混乱,而且你没有获得多少好处。

相反,我使用不同的方法,建立了一个流程。

我们在应用程序中做的很多事情都是流程,是为实现一个结果而需要做的事情的顺序列表。

这也没什么不同。

首先,看一下底层代码--我们想要实现的抽象过程。

abstract class Process
{
    protected array $tasks = [];
 
    public function run(object $payload): mixed
    {
        return Pipeline::send(
            passable: $payload,
        )->through(
            pipes: $this->tasks,
        )->thenReturn();
    }
}

我们要做的就是创建一个我们想要运行的任务集合--这意味着进程可以共享任务而不需要代码重复。

然后,我们通过管道门面运行所有这些。

不过,这与命令和查询有什么关系呢?让我们深入了解一下我们的例子。

final class TeamCreationProcess extends Process
{
    protected array $tasks = [
        CreateNewTeam::class,
        AssignNewTeamMember::class,
        NotifyTeamOwnerOfNewMember::class,
        SetupBillingForTeam::class,
    ];
}

在我们的控制器中,我们所需要做的就是:

final class StoreController
{
    public function __construct(
        private readonly TeamCreationProcess $process,
    ) {}
 
    public function __invoke(StoreRequest $request): Responsable
    {
        $this->process->run($request->payload());
 
        return new MessageResponse(
            message: 'Your team has been created',
        );
    }
}

很干净,对吗?

让我们看一下这些任务中的几个,看看我们在命令和查询方面的倚重。

final class CreateNewTeam
{
    public function __construct(
        private readonly NewTeamCreation $command,
    ) {}
 
    public function __invoke(object $payload, Closure $next): mixed
    {
        $this->command->handle($payload);
 
        return $next($payload);
    }
}

我们的任务可以调用我们的命令,而不是实现同样的逻辑。

你可以在这里添加写入动作。

然而,通过仍然使用一个命令--你可以从API、Web和CLI轻松地创建一个新的团队,而不需要把它包裹在一个过程中。

如果你有一个简单的CLI命令,你很可能想避免一个过程--你想要一个快速的动作。


我现在使用的方法来自于在构建不同类型的应用程序时学到的经验,虽然创建多个类来实现一件事可能有点费力,但随着你的应用程序的发展,你会对这样做表示感谢。

通过将我们需要的东西分解成一个过程,我们可以随着时间的推移通过添加额外的步骤来微调这个过程--而不影响正在发生的事情。

我不知道这种方法是否有一个架构术语,但这是我非常喜欢的一种方法。


我的FormRequest负责创建我想通过的有效载荷。

我的流程负责我想运行的任务。

我的任务负责调用正确的读或写操作,而我的读和写操作只需要担心读或写数据。

这都是小的、构建良好的、可单元测试的代码片断,很容易复制和重构,而不会对我的整个应用产生不利影响。


转:

https://laravel-news.com/going-past-actions-in-laravel

相关文章