在 Laravel 中构建 API

发布时间 作者

Building APIs in Laravel image

在 Laravel 中构建 API 是一门艺术。您必须超越数据访问,将 Eloquent 模型封装在 API 端点中。

您需要做的第一件事是设计您的 API;最好的方法是考虑您的 API 的目的。您为什么要构建这个 API,目标用例是什么?确定好这些之后,您就可以根据 API 的集成方式有效地设计 API。

通过专注于 API 的集成方式,您可以在 API 发布之前消除 API 中的任何潜在痛点。这就是为什么我总是测试我构建的任何 API 的集成,以确保涵盖所有预期用例的顺利集成。

让我们通过一个例子来描述一下。我正在构建一家新的银行,Laracoin。我需要我的用户能够创建帐户并为这些帐户创建交易。我有一个 Account 模型,一个 Transaction 模型,还有一个 Vendor 模型,每个交易都属于它。例如:

Account -> Has Many -> Transaction -> Belongs To -> Vendor
 
Spending Account -> Lunch 11.50 -> Some Restaurant

因此,我们有三个主要模型需要关注我们的 API。如果我们在没有设计引导思维的情况下进行处理,那么我们将创建以下路由

GET /accounts
POST /accounts
GET /accounts/{account}
PUT|PATCH /accounts/{account}
DELETE /accounts/{account}
 
GET /transactions
POST /transactions
GET /transactions/{transaction}
PUT|PATCH /transactions/{transaction}
DELETE /transactions/{transaction}
 
GET /vendors
POST /vendors
GET /vendors/{vendor}
PUT|PATCH /vendors/{vendor}
DELETE /vendors/{vendor}

但是,这些路由的优势是什么?我们只是为 eloquent 模型创建 JSON 访问,这确实有效 - 但没有增加任何价值,而且从集成的角度来看,它让事情感觉很机械。

相反,让我们考虑一下我们 API 的设计和目的。我们的 API 很可能主要由内部移动和 Web 应用程序访问。我们首先会专注于这些用例。知道这一点意味着我们可以微调我们的 API 以适应我们应用程序中的用户旅程。因此,通常情况下,在这些应用程序中,我们会看到一个帐户列表,因为我们可以管理我们的帐户。我们还需要点击进入一个帐户以查看交易列表。然后,我们需要点击交易以查看更多详细信息。我们实际上永远不需要直接查看供应商,因为它们更多地用于分类而不是其他用途。考虑到这一点,我们可以围绕这些用例和原则设计我们的 API

GET /accounts
POST /accounts
GET /accounts/{account}
PUT|PATCH /accounts/{account}
DELETE /accounts/{account}
 
GET /accounts/{account}/transactions
GET /accounts/{account}/transactions/{transaction}
 
POST /transactions

这将使我们能够有效地管理我们的帐户,并且只能通过其所属的帐户直接获取交易。我们现在不希望交易被编辑或管理。这些应该只创建 - 并且从那里,一个内部流程应该在需要时更新这些。

既然我们知道 API 的设计方式,我们就可以专注于如何构建这个 API,以确保它能够快速响应并在复杂性方面扩展。

首先,我们假设我们正在构建一个只包含 API 的 Laravel 应用程序 - 所以我们不需要任何 api 前缀。让我们考虑一下我们如何注册这些路由,因为这通常是应用程序中第一个出现问题的部分。一个繁忙的路由文件在心理上很难解析,认知负荷是任何应用程序中的第一个挑战。

如果这个 API 要面向公众,我会考虑支持版本化的 API,在这种情况下,我会创建一个版本目录,并将每个主要组放在一个专用文件中。但是,我们在这里没有使用版本控制,因此我们将以不同的方式组织它们。

我们要创建的第一个路由文件是 routes/api/accounts.php,我们可以将其添加到我们的 routes/api.php 中。

Route::prefix('accounts')->as('accounts:')->middleware(['auth:sanctum', 'verified'])->group(
base_path('routes/api/accounts.php),
);

每个组都将加载其路由,设置默认的中间件前缀和路由命名模式。我们的 accounts 路由文件将是扁平的,分组最少,除非我们想查看子资源。这使我们能够在尝试理解路由本身时只查看一个区域,但这也意味着任何与帐户有关的内容都将属于此文件。

Route::get(
'/',
App\Http\Controllers\Accounts\IndexController::class,
)->name('index');

我们的第一个路由是帐户索引路由,它将显示经过身份验证用户的全部帐户。除了身份验证路由之外,这可能是通过 API 调用的第一个内容,因此它通常是我首先关注的。关注最关键的路由首先可以解除其他团队的阻塞,而且这还使您可以完善您希望在应用程序中遵循的标准。

现在我们了解了如何路由请求,我们可以考虑如何处理这些请求。逻辑在哪里,如何确保代码重复量最小化?

我最近写了一篇关于如何使用 Eloquent 的有效方法 的教程,其中深入探讨了查询类。这是我偏好的方法,因为它确保代码重复量最少。我不会详细说明为什么我会使用这种方法,因为我在之前的教程中已经详细说明了。但是,我将介绍如何在应用程序中使用它。如果适合您的需求,您可以遵循这种方法。

关键要记住的是,充分利用 API 的最佳方法是按照适合您和您的团队的方式构建它。花几个小时尝试调整不自然的方法只会减慢您的速度,而不会带来您想要实现的效益。

创建查询类时,需要将相应的接口绑定到控制器。这不是一个必需的步骤。但是,我正在写教程 - 所以您期望什么呢?

interface FilterForUserContract
{
public function handle(Builder $query, string $user): Builder;
}

然后是我们想要使用的实现

final class FilterAccountsForUser implements FilterForUserContract
{
public function handle(Builder $query, string $user): Builder
{
return QueryBuilder::for(
subject: $query,
)->allowedIncludes(
include: ['transactions'],
)->where('user_id', $user)->getEloquentBuilder();
}
}

这个查询类将获取所有为传递的用户提供的帐户,允许您选择性地包含每个帐户的交易 - 然后将 eloquent 构建器传递回来,以便在需要时添加其他范围。

然后,我们可以在控制器中使用它来查询经过身份验证用户的帐户,然后在响应中返回它们。让我们看看如何使用这个查询来了解可用的选项。

final class IndexController
{
public function __construct(
private readonly Authenticatable $user,
private readonly FilterForUserContract $query,
) {}
 
public function __invoke(Request $request): Responsable
{
$accounts = $this->query->handle(
query: Account::query()->latest(),
user: $this->user->getAuthIdentifier(),
);
 
// return response here.
}
}

此时,我们的控制器有一个 eloquent 构建器,它将传递给响应,因此在传递数据时,请确保调用 getpaginate 以正确地传递数据。这将我们引入了我的意见性旅程中的下一个阶段。

响应是 API 的主要职责。我们应该快速高效地响应,以便为我们的用户提供快速响应的 API 体验。我们作为 API 的响应方式可以分为两个方面,响应类以及数据如何转换为响应。

这两个方面是响应和 API 资源。我将从 API 资源开始,因为我非常关心它们。API 资源用于屏蔽数据库结构,以及将存储在 API 中的信息转换为最适合在客户端消耗的方式的方法。

我在我的 Laravel API 中使用 JSON:API 标准,因为它是一个非常棒的标准,在 API 社区中得到很好的记录和使用。幸运的是,Tim MacDonald 创建了一个很棒的包,用于在 Laravel 中创建 JSON:API 资源,我在所有 Laravel 应用程序中都依赖它。我最近 写了一篇教程,介绍如何使用这个包,所以这里只介绍一些细节。

让我们从帐户资源开始,它将被设置为具有相关的关系和属性。自从我上次的教程以来,这个包最近更新了,使设置关系变得更加容易。

final class AccountResource extends JsonApiResource
{
public $relationships = [
'transactions' => TransactionResource::class,
];
 
public function toAttributes(Request $request): array
{
return [
'name' => $this->name,
'balance' => $this->balance->getAmount(),
];
}
}

现在,我们将保持简单。我们希望返回帐户名称和余额,并可以选择加载交易关系。

使用这些资源意味着要访问名称,我们必须使用:data.attributes.name,这可能需要一段时间才能在您的 Web 或移动应用程序中习惯,但您很快就会上手。我喜欢这种方法,因为我们可以分离关系和属性,并在需要时扩展它们。

一旦我们的资源填充完毕,我们就可以专注于其他领域,例如授权。这是我们 API 的一个重要部分,不容忽视。我们大多数人之前都使用过 Laravel 的 Gate,使用过 Gate Facade。然而,我喜欢从框架本身注入 Gate 契约。这主要是因为在有机会的时候,我更喜欢依赖注入而不是 Facade。让我们来看看这在 StoreController 中对于账户来说会是什么样子。

final class StoreController
{
public function __construct(
private readonly Gate $access,
) {}
 
public function __invoke(StoreRequest $request): Responsable
{
if (! $this->access->allows('store')) {
// respond with an error.
}
 
// the rest of the controller goes here.
}
}

这里我们只是像使用 Facade 一样使用 Gate 功能,因为它们是同一个东西。我在此使用 allows,但你可以使用 can 或其他方法。你应该关注授权,而不是它的实现方式,因为这对于你应用程序来说只是一个次要细节。

所以我们知道我们希望数据在 API 中如何表示,以及我们希望在应用程序中如何授权用户。接下来,我们可以看看如何处理写入操作。

对于我们的 API 来说,写入操作至关重要。我们需要确保它们速度快,因为它们可以使我们的 API 感觉敏捷。

你可以通过多种方式在 API 中写入数据,但我更喜欢使用后台作业并快速返回。这意味着你可以不用担心客户端,在自己的时间内处理创建事物的方式的逻辑。好处是你的后台作业仍然可以通过 WebSockets 发布更新,以获得实时效果。

让我们看看更新后的 StoreController,当我们使用这种方法时,它适用于账户。

final class StoreController
{
public function __construct(
private readonly Gate $access,
private readonly Authenticatable $user,
) {}
 
public function __invoke(StoreRequest $request): Responsable
{
if (! $this->access->allows('store')) {
// respond with an error.
}
 
dispatch(new CreateAccount(
payload: NewAccount::from($request->validated()),
user: $this->user->getAuthIdentifier(),
));
 
// the rest of the controller goes here.
}
}

我们向后台作业发送一个数据传输对象的有效负载,该对象将在队列上进行序列化。我们使用验证后的数据创建了这个 DTO,并希望通过用户 ID 发送它,因为我们需要知道为谁创建它。

遵循这种方法,我们有有效的、类型安全的数据被传递以创建模型。在我们的测试中,我们这里只需要确保作业被分派。

it('dispatches a background job for creation', function (string $string): void {
Bus::fake();
 
actingAs(User::factory()->create())->postJson(
uri: action(StoreController::class),
data: [
'name' => $string,
],
)->assertStatus(
status: Http::ACCEPTED->value,
);
 
Bus::assertDispatched(CreateAccount::class);
})->with('strings');

我们在这里进行测试,以确保我们通过了验证,从我们的 API 中获得了正确的状态码,然后确认正确的后台作业被分派。

在此之后,我们可以隔离地测试作业,因为它不需要包含在我们的端点测试中。现在,它将如何写入数据库?我们使用一个 Command 类来写入我们的数据。我使用这种方法是因为仅仅使用 Action 类会很乱。最终我们会得到数百个 Action 类,在我们的目录中寻找一个特定的类时,很难解析它们。

与往常一样,因为我喜欢使用依赖注入,我们需要创建用于解析实现的接口。

interface CreateNewAccountContract
{
public function handle(NewAccount $payload, string $user): Model;
}

我们使用 New Account DTO 作为有效负载,并将用户 ID 作为字符串传递。通常,我把它作为字符串;我会在我的应用程序中为 ID 字段使用 UUID 或 ULID。

final class CreateNewAccount implements CreateNewAccountContract
{
public function handle(NewAccount $payload, string $user): Model
{
return DB::transaction(
callback: fn (): Model => Account::query()->create(
attributes: [
...$payload->toArray(),
'user_id' => $user,
],
),
);
}
}

我们将写入操作封装在数据库事务中,这样只有在写入成功的情况下才会提交到数据库。如果写入不成功,它允许我们回滚并抛出异常。

我们已经涵盖了如何为我们的响应转换模型数据、如何查询和写入数据,以及我们希望如何在应用程序中授权用户。在 Laravel 中构建可靠 API 的最后阶段是看看我们如何作为 API 进行响应。

大多数 API 在响应方面都很糟糕。这很讽刺,因为它可能是 API 最重要的部分。在 Laravel 中,你可以通过多种方式进行响应,从使用辅助函数到返回 JsonResponse 的新实例。但是,我喜欢构建专门的响应类。它们类似于 Query 和 Command 类,旨在减少代码重复,但也是返回响应最可预测的方式。

我创建的第一个响应是集合响应,我将在返回经过身份验证的用户拥有的帐户列表时使用它。我还将创建其他响应的集合,从单个模型响应到空响应和错误响应。

class Response implements Responsable
{
public function toResponse(): JsonResponse
{
return new JsonResponse(
data: $this->data,
status: $this->status->value,
);
}
}

首先,我们必须创建我们的响应类将扩展的初始响应。这是因为它们都将以相同的方式响应。它们都需要以相同的方式返回数据和状态码。所以现在,让我们看看集合响应类本身。

final class CollectionResponse extends Response
{
public function __construct(
private readonly JsonApiResourceCollection $data,
private readonly Http $status = Http::OK,
) {}
}

这非常简洁易于实施,你可以将 data 属性转换为联合类型,使其更灵活。

final class CollectionResponse extends Response
{
public function __construct(
private readonly Collection|JsonResource|JsonApiResourceCollection $data,
private readonly Http $status = Http::OK,
) {}
}

这些简洁易懂,所以让我们看看 IndexController 的最终实现,它适用于账户。

final class IndexController
{
public function __construct(
private readonly Authenticatable $user,
private readonly FilterForUserContract $query,
) {}
 
public function __invoke(Request $request): Responsable
{
$accounts = $this->query->handle(
query: Account::query()->latest(),
user: $this->user->getAuthIdentifier(),
);
 
return new CollectionResponse(
data: $accounts->paginate(),
);
}
}

关注这些关键领域,可以让你扩展 API 的复杂性,而不必担心代码重复。当试图找出导致 Laravel API 速度慢的原因时,这些是我始终关注的关键领域。

这绝不是一个详尽的教程或你需要关注的清单,但是遵循这个简短的指南,你可以为未来的成功做好准备。

Steve McDougall photo

Laravel 新闻 的技术作家,Treblle 的开发者倡导者。API 专家,资深 PHP/Laravel 工程师。 YouTube 直播主播.

分类于
Cube

Laravel 新闻通讯

加入 40,000 多名其他开发者,绝不错过任何新技巧、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理服务器,并在几秒钟内部署 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发者的必备代码运行器。使用 AI、自动完成和对本地和生产环境的即时反馈进行调试。

Tinkerwell
No Compromises logo

No Compromises

Joel 和 Aaron,来自 No Compromises 播客的两位经验丰富的开发人员,现在可供雇佣,为您的 Laravel 项目提供服务。 ⬧ 固定价格 7500 美元/月。 ⬧ 无冗长的销售流程。 ⬧ 无合同。 ⬧ 100% 退款保证。

No Compromises
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保您的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?即时、自动化的 Laravel 升级和代码现代化,让您的应用程序保持新鲜。

Shift
Bacancy logo

Bacancy

让您的项目充满活力,只需 2500 美元/月,即可获得一位经验丰富的 Laravel 开发者,拥有 4-6 年的经验。获得 160 小时的专业知识和 15 天的免费试用期。立即预约通话!

Bacancy
Lucky Media logo

Lucky Media

现在就获得幸运 - Laravel 开发的理想选择,拥有十多年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel 电子商务

Laravel 的电子商务。一个开源包,将现代无头电子商务功能的强大功能引入 Laravel。

Lunar: Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 工作板

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS 启动工具包

SaaSykit 是一个 Laravel SaaS 启动工具包,它包含运行现代 SaaS 所需的所有功能。付款、精美结账、管理面板、用户仪表板、身份验证、就绪组件、统计、博客、文档等。

SaaSykit: Laravel SaaS 启动工具包
Rector logo

Rector

您无缝 Laravel 升级的合作伙伴,降低成本,加速创新,助力企业成功

Rector
MongoDB logo

MongoDB

通过强大的 MongoDB 和 Laravel 集成来增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。了解 MongoDB 灵活、现代的数据库如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的零依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel 提示构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 包为您的 Laravel 应用程序添加评论

阅读文章