Laravel API Resources
Laravel 可以直接将 Eloquent 模型转换成 JSON,但数据库字段并不总适合原样返回给客户端。字段名称、金额格式、关联数据和内部状态都可能需要单独处理。API Resources 可以在模型与接口响应之间增加一层转换,让输出结构更加清晰,也便于后续维护。
以下示例采用 Laravel 12,以商品接口为例,介绍单个资源、列表集合、关联资源、条件字段和分页响应的处理方法。
一、API Resources 解决什么问题
直接返回模型的写法通常是:
return Product::findOrFail($productId);
这种方式简洁,但接口输出容易与模型的序列化配置紧密耦合。数据库或模型配置发生变化时,接口也可能出现非预期变化。
使用 API Resource 后,可以明确指定对外提供哪些字段:
Product 模型
↓
ProductResource
↓
客户端收到的 JSON
Resource 适合处理字段选择、字段重命名、格式转换和关联数据组织。
Resource 不代替权限检查。 用户能否访问某个商品、订单或账户,仍然需要通过认证、Policy 或其他授权逻辑判断。
二、准备示例模型关系
以下示例假设已有商品和分类模型,字段如下。
商品表 products:
id
category_id
name
description
price_cents
created_at
updated_at
分类表 categories:
id
name
其中,price_cents 使用整数保存人民币分值,例如 19900 表示 199 元。
在 Product 模型中定义分类关系:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Product extends Model
{
protected function casts(): array
{
return [
'price_cents' => 'integer',
];
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}
已有模型应合并所需方法,保留原来的其他配置。
三、创建单个商品 Resource
执行:
php artisan make:resource ProductResource
文件生成在:
app/Http/Resources/ProductResource.php
编写以下内容:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'price_cents' => $this->price_cents,
'currency' => 'CNY',
'created_at' => $this->created_at?->toISOString(),
];
}
}
这里明确列出返回字段,没有自动输出模型中的全部属性。
金额继续以整数分值返回,并附带币种,客户端可按展示需求格式化。若实际业务采用其他币种或金额单位,需要相应调整。
四、在控制器中返回 Resource
创建控制器:
php artisan make:controller Api/ProductController
在控制器中编写详情方法:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\ProductResource;
use App\Models\Product;
class ProductController extends Controller
{
public function show(Product $product): ProductResource
{
return new ProductResource($product);
}
}
在已经注册的 routes/api.php 中添加:
use App\Http\Controllers\Api\ProductController;
use Illuminate\Support\Facades\Route;
Route::get('/products/{product}', [ProductController::class, 'show']);
Laravel 12 的新项目如果尚未启用 API 路由,可以先执行:
php artisan install:api
该命令会安装 Sanctum 并建立 API 路由相关配置。已有 API 路由配置的项目无需重复执行。
这里使用公开商品作为示例。正式项目应在查询和授权层限制可见范围,避免公开下架商品、内部记录或其他不应对外展示的数据。
启动本地开发服务后:
php artisan serve
请求一条实际存在的商品:
curl -H "Accept: application/json" \
http://127.0.0.1:8000/api/products/1
默认情况下,Resource 响应会使用 data 包裹:
{
"data": {
"id": 1,
"name": "示例商品",
"description": "商品介绍",
"price_cents": 19900,
"currency": "CNY",
"created_at": "2026-09-10T08:00:00.000000Z"
}
}
具体值以数据库记录为准。项目如果修改过 Resource 的全局包裹设置,响应结构也可能不同。
五、返回商品列表和分页结果
为控制器增加列表方法:
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
public function index(): AnonymousResourceCollection
{
$products = Product::query()
->orderByDesc('id')
->paginate(15);
return ProductResource::collection($products);
}
然后在 routes/api.php 中添加:
Route::get('/products', [ProductController::class, 'index']);
请求列表:
curl -H "Accept: application/json" \
"http://127.0.0.1:8000/api/products?page=2"
将分页结果传入 ProductResource::collection() 后,Laravel 会保留分页所需信息。默认响应包括:
data:当前页的商品列表。links:首页、末页、上一页和下一页链接。meta:当前页码、每页数量和总记录数等信息。
普通列表通常不需要创建独立的 Resource Collection 类。只有集合级别的结构或元数据需要特别处理时,再增加专用集合类。
六、转换关联分类数据
如果商品接口需要同时返回分类信息,先创建分类 Resource:
php artisan make:resource CategoryResource
内容如下:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class CategoryResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
];
}
}
然后在 ProductResource 的返回数组中增加:
'category' => CategoryResource::make(
$this->whenLoaded('category')
),
详情控制器加载分类关系:
public function show(Product $product): ProductResource
{
$product->loadMissing('category');
return new ProductResource($product);
}
列表查询则使用预加载:
public function index(): AnonymousResourceCollection
{
$products = Product::query()
->with('category')
->orderByDesc('id')
->paginate(15);
return ProductResource::collection($products);
}
whenLoaded('category') 仅在关系已经加载时包含该字段,不会为了序列化而主动查询分类。
关联数据由查询层决定是否加载,Resource 负责转换。 这样更容易控制 SQL 数量,避免列表中的每个商品分别查询一次分类。
七、按请求条件返回字段
列表接口未必需要完整的商品描述。可以约定只有请求包含 include_description=1 时,才返回 description。
在 ProductResource 中,将原来的 description 字段替换为:
'description' => $this->when(
$request->boolean('include_description'),
fn () => $this->description
),
测试:
curl -H "Accept: application/json" \
"http://127.0.0.1:8000/api/products?include_description=1"
没有传入参数或参数被解析为 false 时,description 字段会从响应中移除。
这种请求参数适合控制普通展示内容。成本价、内部备注等敏感字段,必须根据服务端授权结果判断,不能仅凭客户端传入参数决定是否返回。
如果列表使用筛选或展示参数,希望翻页链接保留这些参数,可以在分页后追加经过确认的参数:
$products = Product::query()
->with('category')
->orderByDesc('id')
->paginate(15)
->appends(
request()->only(['include_description'])
);
八、添加集合级元数据
如果前端需要识别当前响应的业务类型,可以使用 additional() 添加顶层元数据:
return ProductResource::collection($products)
->additional([
'resource_type' => 'products',
]);
这会在原有响应结构中增加顶层的 resource_type 字段,并保留分页数据。
额外字段应服务于明确的接口约定。已经由 HTTP 状态码、分页 meta 或业务字段表达的信息,不必重复包装多层。
九、验证接口输出
完成配置后,可以按以下顺序检查:
- 详情接口:只返回 Resource 中声明的字段。
- 列表接口:每条记录使用相同的字段结构。
- 分页结果:
links和meta能够正确表示当前页。 - 关联数据:加载分类时返回分类,没有加载时不额外查询。
- 条件字段:
include_description能够控制描述字段。 - 访问权限:未授权用户无法获取受限记录。
Resource 层的字段测试可以使用以下断言形式:
$response->assertOk()
->assertJsonStructure([
'data' => [
'id',
'name',
'price_cents',
'currency',
],
])
->assertJsonMissingPath('data.cost_price');
其中,$response 应来自测试中对真实接口的请求。敏感字段断言应替换为项目实际需要保护的字段。
广告合作
QQ群号:4114653
- 本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。邮箱:2942802716#qq.com(#改为@)。
- 本站原创内容未经允许不得转载,转载请注明出处“站长百科”和原文地址。