编程

Laravel 响应类 Response

1289 2023-04-10 16:00:00

Laravel 应用的响应至关重要,特别是当你在构建 API 时。

通常很多人都是从 helper 函数开始,因为文档和许多教程都这么使用。这样做很容易入手,且能准确达成你期望的目标。如下:

return response()->json(
    data: [],
    status: 200,
);

这是一个稍微夸张的例子;你通常发送数据并跳过状态代码。然而,对我来说,习惯很难改掉!

这个代码会为你创建一个新的 JsonResponse 对象,并传入数据和状态码。这是有效的,使用这种方法没有错。如果您已经在使用了,那么在这里加强API游戏的一种方法是添加状态代码,以便在返回的内容中更具声明性。

再进一步,我们可以跳过 helper 函数,开始使用该helper函数所依赖的底层类进行创建:

return new JsonResponse(
    data: [],
    status: 200,
);

我喜欢这种方式,因为它对 helper 函数依赖更少,且更具有声明性。通过查看代码,你可以确切地知道返回的内容,因为它就在你面前,而不是被抽象到 helper 后面。你可以通过使用常量或其他方式来声明状态代码本身,从而使可能不知道所有状态代码的开发人员更容易阅读和理解状态代码。让我们看看这可能是什么样子:

return new JsonResponse(
    data: [],
    status: JsonResponse::HTTP_OK,
);

JsonResponse 类通过几个抽象层扩展了 Symfony Response 类,这样您就可以直接调用它——然而,你的静态分析器可能会抱怨这一点。我构建了一个名为 juststeveking/http 状态代码的包,这是一个将返回类似内容的 PHP Enum,它唯一的工作就是返回状态代码。我更喜欢这种更轻量级的实用程序方法,因为你确切地知道发生了什么,以及这个类或包可能会做什么。有时问题是,你正在使用的类做得太多了,以至于你必须将这个巨大的东西加载到内存中,才能返回一个整数值。这没有多大意义,所以我建议使用专用的包或类来自己管理。让我们看看当你这样做时会是什么样子:

return new JsonResponse(
    data: [],
    status: Http::OK->value,
);

就明确我们的代码的声明性而言,这是向前迈出的重要一步。很容易阅读和理解到底发生了什么。然而,我们发现自己一次又一次地创建相同的代码块,那么我们如何解决这个问题呢?

答案很简单——响应类。在 Laravel 中,我们可以使用一个名为 Responsable 的 contract,它告诉我们类必须有一个 toResponse 方法。我们可以直接从控制器返回,因为 Laravel 将毫无问题地解析和理解这些类。让我们来看看这些类的基本示例:

class MyJsonResponse implements Responsable
{
    public function __construct(
        public readonly array $data,
        public readonly Http $status = Http::OK,
   ) {}
 
    public function toResponse($request): Response
    {
        return new JsonResponse(
            data: $this->data,
            status: $this->status->value,
        );
    }
}

这是一个简单易用的东西。然而,它并没有为我们的应用程序增加任何价值。这只是对已经存在的东西的抽象。让我们看看可能为我们的应用程序增加更多价值的东西。

class CollectionResponse implements Responsable
{
    public function __construct(
        public readonly JsonResourceCollection $data,
        public readonly Http $status = Http::OK,
    ) {}
 
    public function toResponse($request): Response
    {
        return new JsonResponse(
            data: $this->data,
            status: $this->status->value,
        );
    }
}

现在,我们有了一个响应类,它将处理我们经过的任何资源集合,使其对我们的应用程序非常可重用。让我们看看如何在控制器中返回此信息:

return new CollectionResponse(
    data: UserResource::collection(
        resource: User::query()->get(),
    ),
);

它更干净,代码重复更少,而且如果需要的话,很容易覆盖默认状态。它给我们带来了 helper 方法和 Json Response 类给我们带来的好处,但让我们有了更多的上下文和可预测性。

然而,我们现在面临着其他区域的代码重复问题。在我们的响应类中。其中许多看起来很相似,唯一的区别是构造函数属性将是不同的类型。我们希望保留使用自定义响应类的上下文,但我们希望避免为属性创建具有大量联合类型参数的东西——如果我们可以加入 mixed 类型并完成它时。

在这种情况下,你可以选择一个抽象类来扩展,也可以选择用 trait 来将行为添加到需要它的类中。就我个人而言,我喜欢组合而不是继承,所以使用 trait 对我来说更有意义。

trait SendsResponse
{
    public function toResponse($request): Response
    {
        return new JsonResponse(
            data: $this->data,
            status: $this->status->value,
        );
    }
}

这种方法最大的问题是静态分析会抱怨这段代码,因为 trait 需要拥有或知道类的属性。不过,这是一个很容易解决的问题。

/**
 * @property-read mixed $data
 * @property-read Http $status
 */

我们可以将这个 doc 块添加到 trait,以便它知道它可以访问的属性。

现在,我们的响应类的使用和构建将简单得多,代码中的重复更少。

class MessageResponse implements Responsable
{
    use SendsResponse;
 
    public function __construct(
        public readonly array $data,
        public readonly Http $status = Http::OK,
    ) {}
}

现在,我们可以构建所有需要轻松发送的潜在响应,从而提高类型安全性,减少代码重复。