Laravel 中实现 SpringBoot 中的 RequestBody 注解

如果你使用过 Java 的 SpringBoot 框架,会不会感觉到当中某些注解很好用呢?比如说 @GetMapping@PostMapping 之类的路由注解,@Autowired 可以通过属性注入依赖。

当然,还有这篇文章的主角 @RequestBody 注解,它可以将客户端请求中 application/json 格式的参数转换为 Java 中的 DTO(Data Transfer Object)。

PHP 中的示例

这里就不贴 Java 的代码了,来看 PHP 中的实例:`

class UserController
{
    public function register(#[RequestBody] RegisterParams $params)
    {
        dump($params->getUsername(), $params->getPassword());
    }
}

通过 #[RequestBody] 注解,可以自动将前端的请求参数写入到 RegisterParams 这个类中,然后在代码中通过这个类暴露出来的 Getter 方法来调用。这样的写法有如下这些好处:

  1. 改变传统上我们对 PHP 数组的严重依赖 ,虽然 PHP 中的数组非常强大,但是在 IDE 提示是不足的,且单词拼写错误也难以发觉,例如我曾将 build 拼写成了 built ,这样的代码只有在运行时、测试中,甚至上线后才能发现错误。
  2. 让我们写代码更加流程 ,是的,这是我写 Java 代码最大的一个感受,流畅!IDE 对代码的提示的非常精准,基本上是一路回车来完成代码。但是如果你依赖 PHP 中的数组,那么写代码会经常性停顿,去回忆、去猜下一个 Key 是什么?

得益于 PHP8 中提供的参数注解,实现 #[RequestBody] 有了可能。但要想在 Laravel 中实际实现,还是需要动动脑经的。请看下文分解这个注解的实现过程:

  1. 创建路由中间件,拦截请求
  2. 在中间件中解析请求的控制器方法中的 #[RequestBody] 注解
  3. request()->all() 中获取到的参数通过反射写入到其修饰的参数的类型中(DTO 对象)
  4. 将 DTO 对象注入到 Laravel 的容器中
  5. 当执行到控制器的方法时,Laravel 通过从容器中获取我们事先注入的 DTO 对象注入控制器方法的参数中。

实现 #[RequestBody] 注解

实现 #[RequestBody] 注解依赖 Laravel 的路由中间件,中间件的基础代码如下:

class RequestBodyInjector
{
    public function handle(Request $request, Closure $next): mixed
    {
        $route = $request->route();
        if ($route === null) {
            return $next($request);
        }
        $controller = $route->getController();
        $action = $route->getActionMethod();
        // todo: 接着实现注解的解析

        return $next($request);
    }
}

上面的代码很简单,就是获取路由,然后从路由中获取 Controller 以及 Action 的名字,因为你要去解析这个方法的参数。解析的代码如下:

public function handle(Request $request, Closure $next): mixed
{
    // 省略之前获取 Controller 和 ActionMethod 的代码
    // 通过反射获取控制器方法中的参数
    $reflectionMethod = new ReflectionMethod($controller, $action);
    $parameters = $reflectionMethod->getParameters();
    // 遍历参数
    foreach ($parameters as $parameter) {
        // 检查是否存在 #[RequestBody] 的注解
        $attributes = collect($parameter->getAttributes(RequestBody::class));
        if ($attributes->isEmpty()) {
            continue;
        }
        // 存在则获取参数写入 DTO 对象
        $this->addBodyToRequest($request, $parameter);
    }
    return $next($request);
}

最后,就是实现上面的 addBodyToRequest() 方法了:

private function addBodyToRequestIfBean(Request $request, ReflectionParameter $parameter): void
{
    // 获取请求参数
    $params = $request->input();
    // 实例化 DTO 对象,将参数传入
    $dto = new ($parameter->getType()->getName())($params);
    // 关键:注入到容器
    app()->instance($parameter->getType()->getName(), $bean);
}

上面实例化 DTO 对象,传入参数这一步我可以给出一个简单的实现:

class DTO
{
    public function __construct(array $data = [])
    {
        foreach ($data as $key => $value) {
            $this->$key = $value;
        }
    }
}

其他的对象都继承 DTO 就可以了。

总结

通过 #[RequestBody] 的封装,我们在编写优雅的代码的路上又前进一步。

虽然封装的过程有点小复杂,你需要理解 PHP 中的注解、反射,你还需要理解 Laravel 中的容器、依赖注入、控制反转、中间件等概念。但是使用简单,你只需要在控制器方法中加上一个注解就可以了。这也是封装的意义:允许封装复杂,但是使用要简单

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 4

可能你会感到疑惑,使用 request()->all() 就可以完成的事情,非要弄的那么复杂?

2周前 评论
苏近之 (作者) (楼主) 2周前

牛的 有点鸡肋

2周前 评论
苏近之 (楼主) 2周前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!