编程

控制器及其真实意图

810 2024-01-24 04:19:00

重构 (UI) Controller

首先,我们先花一分钟使用  Service Location (SL) 重构一下代码:

final readonly class SubmitContactFormController
{
    public function __invoke(SubmitRequest $request): RedirectResponse
    {
        $command = new ContactMuhammed(
            $request->input('email'),
            $request->ip(),
            $request->input('message'),
            $request->input('name'),
        );
        
        Bus::dispatch($command);

        return Redirect::route('contact', ['success' => true]);
    }
}

我们采用的步骤为:

  • 表单验证规则被移动到 FormRequest
    • Bus 组件现在通过一个静态的容器代理使用
    • ResponseFactory 组件现在通过一个静态的容器代理使用

既然我们通过 SL 成功重构了(UI)控制器,我们开始来探讨其意图吧。

(UI) 控制器作为组合根(Composition Root)

在应用的生命周期中,(UI)控制器实际上扮演着一个特殊的角色。web 服务器通常会收到一个请求,并将其转发给 PHP 进程,该进程会启动框架,最后框架会将请求转发给:(UI)控制器。基于此,我们可以确定一个事实,即(UI)控制器是应用的组合根(Composition Root)。这是我们可以完全控制的第一段被调用的代码。

这就是为什么我真的不介意你在(UI)控制器中使用 DI 还是 SL 的原因。对象图必须以某种方式组成,所以可以随意使用它们中的任何一个。

为什么要在 Controller 上添加 UI 前缀?

我很高兴这样问。这是因为我想强调一个事实:UI 控制器的任务是协调请求-响应生命周期,该生命周期通常由用户通过用户界面启动。这里的关键字是编排。它的主要目标应该是处理与 UI 相关的问题,如表单验证、渲染视图、创建重定向等。如果控制器坚持其真正的目的,那么它是 10 行还是 90 行就无关紧要了。它应该处理 UI 问题,并且处理好。其他所有内容都不属于控制器内部,应转发给 Application

这听起来可能有点违背直觉,但 CLI 命令也是控制器。它还接受用户输入并用它做一些事情,尽管方式略有不同。Livewire 组件?是的,他们也是控制器。只是在前端利用 XHR 的动态系统。

将消息转发到应用

当我们的控制器收到请求时,一定会发生一些事情。有人试图调用我们系统中的某些特定行为。用户的意图由命令对象表示,或者换句话说,应用由这一单个命令对象表示:

$command = new ContactMuhammed(
    $request->input('email'),
    $request->ip(),
    $request->input('message'),
    $request->input('name'),
);

控制器与应用的关系在此开始和结束。它将消息(在本例中为命令)转发给应用且到此为止。ContactMuhammed 是控制器和处理此命令的应用之间的契约。只要契约得到尊重并保持完整,一切都会顺利进行。这就是所谓的“松散耦合”。

现在,我有意使应用尽可能抽象,因为实现细节可能因人而异,也可能因代码库而异。有些人喜欢实现干净的体系结构(我称之为 “Baklava” 架构),有些人喜欢垂直切片他们的应用,还有一些人喜欢混合和匹配。

那不是 ”Action" 模式吗?

不是。Action 模式是 GoF Command 模式的重新命名版本,它表示一个自处理命令。

如果我们仔细观察 ContactMuhammed,我们可以看到它内部嵌入了 0 个业务逻辑:

final readonly class ContactMuhammed implements ShouldQueue
{
    public function __construct(
        public string $email,
        public string $ipAddress,
        public string $message,
        public string $name,
    ) {}
}

ContactMuhammed 就是我们所说的 EIP 命令。它代表了用户的意图,而且仅此而已。没什么了。眼尖的读者可能已经注意到,它实际上也是一个数据传输对象,尽管是一个更具体的对象。

那么查询方面呢?

到目前为止,我们确实只讨论过命令。然而,查询一些数据并将其返回给用户并不会改变有关控制器设计的任何内容。虽然命令可以由应用异步处理,但查询通常是同步的,因此我们在时间上与应用耦合。

这就是负责渲染的逻辑:

final readonly class ReadBlogPostController
{
    public function __construct(
        private GetSinglePost $posts, 
        private ResponseFactory $response,
    ) {}

    public function __invoke(string $slug): Response
    {
        $post = $this->posts->findBySlug($slug);

        return $this->response->view('read-blog-post', $post->toArray());
    }
}

GetSinglePost 是控制器(Controller)和应用(Application)之间的契约。只要它继续返回一个 Post 视图模型,一切都将保持正常运行,不会有任何中断。

总结

  • 控制器(Controller)主要任务是处理用户界面
  • 控制器(Controller)应该将其他事情委托给应用
  • 控制器(Controller)应该在失败后返回用户友好的错误信息