超越 Laravel 中的动作
发布于 作者: 史蒂夫·麦克杜格尔
在过去的一年左右的时间里,基于动作的方法在 Laravel 世界中越来越受欢迎。我非常喜欢它,并在早期就采用了它。
然而,随着时间的推移,我发现我将所有能想到的东西都塞进了 Actions 命名空间,这个目录越来越臃肿。我从试图简化我的流程,将所有东西都提取到动作中,结果发现每次寻找合适的动作都越来越复杂。
我决定做点什么,找到一种方法,既能让我继续从这种方法中获益,又能避免将所有东西都归类为动作。之前使用过各种架构模式,我想起了其中一些对我很有用的模式。我非常喜欢 CQRS(命令查询职责分离)。但是,我不喜欢手动添加所有映射的方法,这样我就可以依赖于命令总线或查询总线。但是我可以从这个模式中借鉴我喜欢的部分,并依靠 Laravels 容器来完成我需要做的事情。
因此,我决定将我归类为动作的东西拆分成更接近 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 中建模业务流程 的教程,您就会理解这个过程。假设我们有一个流程,我们希望为我们的用户创建一个新团队。我们为此采取的步骤如下
- 创建一个新的团队模型。
- 将我们的用户作为团队成员添加。
- 通过电子邮件通知我们的用户。
- 设置团队所需的任何其他资源,例如账单。
如果您在控制器中查看它,它会很大,如果使用动作甚至命令和查询,您最终会将许多东西拉到一个地方,命名会变得混乱,您不会获得太多好处。相反,我使用了一种不同的方法,并构建了一个流程。我们应用程序中的许多操作都是流程,一个需要完成的顺序列表,以达到一个结果。这没有什么不同。首先,看看底层的代码,即我们要实现的抽象流程。
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 负责创建我想要传递的负载。我的流程负责我要运行的任务。我的任务负责调用正确的读取或写入操作,而我的读取和写入操作只需要关心读取或写入数据。所有这些都是小巧、结构良好且易于测试的代码片段,易于复制和重构,而不会对我的整个应用程序造成不利影响。