Laravel: Laravel API Resources与RESTful API开发
最后更新:2026-08-26
API Resources 是 Laravel 的"数据包装器"——控制哪些字段暴露给客户端、如何格式化、关联怎么嵌套,API 输出从此统一规范。
1. 你将学到
- Resource 类与 ResourceCollection:数据转换与格式化
- 关联资源嵌套:whenLoaded() 条件加载关联
- API Resource 分页、过滤与排序封装
- RESTful API 设计规范:URI/动词/状态码/错误格式
- API 版本控制:v1/v2 路由分组与资源继承
2. 一个前端开发者的真实故事
(1) 痛点:API 返回的 JSON 格式让前端崩溃
Alice 对接 ShopMetrics API 时遇到了噩梦——/shops 返回 {shops: [...]} 但 /orders 返回 {data: [...]},/products 直接返回数组。字段名也不统一:有的用 created_at,有的用 createdAt,有的用 createdDate。密码和内部 ID 也暴露在 JSON 里。前端适配代码比业务逻辑还多。
(2) API Resources 的解法
API Resources 统一控制 JSON 输出格式——字段名一致、敏感字段隐藏、关联条件嵌套、分页格式标准。
PHP
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'status' => $this->status,
'products' => ProductResource::collection($this->whenLoaded('products')),
'created_at' => $this->created_at->toISOString(),
];
}
}
(3) 收益
Alice 用 API Resources 后,所有端点格式统一,前端适配代码减少 80%,密码等敏感字段自动隐藏。
3. Resource 类与 ResourceCollection
(1) 创建 Resource
BASH
php artisan make:resource ShopResource
php artisan make:resource ProductResource
php artisan make:resource OrderResource
# Creates: app/Http/Resources/ShopResource.php
(2) Resource 类(单条数据)
PHP
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'description' => $this->description,
'status' => $this->status,
'revenue' => (float) $this->revenue,
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
];
}
}
// Usage — single resource
return new ShopResource($shop);
// {"data": {"id": 1, "name": "Alice Store", ...}}
(3) ResourceCollection(多条数据)
PHP
// Using resource collection
return ShopResource::collection($shops);
// {"data": [...], "links": {...}, "meta": {...}}
// Custom collection
php artisan make:resource ShopCollection
class ShopCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total_shops' => $this->collection->count(),
],
];
}
}
| 类型 | 返回格式 | 适合 |
|---|---|---|
new Resource($model) |
{data: {...}} |
单条记录 |
Resource::collection($models) |
{data: [...], links, meta} |
列表+分页 |
CustomCollection |
自定义格式 | 需要额外 meta |
▶ 示例:ShopMetrics ShopResource
PHP
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'description' => $this->whenNotNull($this->description),
'status' => $this->status,
'revenue' => [
'raw' => (float) $this->revenue,
'formatted' => $this->revenue_formatted,
],
'products_count' => $this->whenCounted('products'),
'orders_count' => $this->whenCounted('orders'),
'products' => ProductResource::collection($this->whenLoaded('products')),
'latest_order' => new OrderResource($this->whenLoaded('latestOrder')),
'links' => [
'self' => route('api.v1.shops.show', $this->id),
'products' => route('api.v1.products.index', ['shop_id' => $this->id]),
],
'created_at' => $this->created_at->toISOString(),
];
}
}
输出:
TEXT
📖 仅展示
// 执行成功
4. 关联资源嵌套
(1) whenLoaded() 条件加载
PHP
// Only include relation if it was eager-loaded
'products' => ProductResource::collection($this->whenLoaded('products')),
// If products were not loaded with(), this returns null and is omitted
// Shop::find(1) → no products key in JSON
// Shop::with('products')->find(1) → products included
// Single relation
'user' => new UserResource($this->whenLoaded('user')),
// Count only (no data)
'products_count' => $this->whenCounted('products'),
(2) 嵌套资源定义
PHP
// app/Http/Resources/OrderResource.php
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'order_number' => $this->order_number,
'status' => $this->status,
'total' => (float) $this->total,
'items' => OrderItemResource::collection($this->whenLoaded('items')),
'shop' => new ShopBriefResource($this->whenLoaded('shop')),
'user' => new UserBriefResource($this->whenLoaded('user')),
'created_at' => $this->created_at->toISOString(),
];
}
}
// Brief resource — minimal data for nesting
class ShopBriefResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
];
}
}
| 方法 | 说明 | 避免问题 |
|---|---|---|
whenLoaded() |
关联已加载才输出 | 避免 N+1 懒加载 |
whenCounted() |
已计数才输出 | 避免额外查询 |
whenNotNull() |
非 null 才输出 | 清理空字段 |
when() |
条件输出 | 灵活控制 |
| Brief Resource | 嵌套用精简版 | 避免循环嵌套 |
▶ 示例:ShopMetrics 嵌套资源
PHP
// app/Http/Resources/OrderItemResource.php
class OrderItemResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'product' => new ProductBriefResource($this->whenLoaded('product')),
'quantity' => $this->quantity,
'unit_price' => (float) $this->price,
'subtotal' => (float) ($this->price * $this->quantity),
];
}
}
// app/Http/Resources/ProductBriefResource.php
class ProductBriefResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'sku' => $this->sku,
];
}
}
输出:
TEXT
📖 仅展示
// 执行成功
5. RESTful API 设计规范
(1) URI 与 HTTP 动词
| 操作 | HTTP | URI | 说明 |
|---|---|---|---|
| 列表 | GET | /api/v1/shops | 返回集合 |
| 创建 | POST | /api/v1/shops | 创建资源 |
| 详情 | GET | /api/v1/shops/{id} | 返回单条 |
| 全量更新 | PUT | /api/v1/shops/{id} | 替换资源 |
| 部分更新 | PATCH | /api/v1/shops/{id} | 修改字段 |
| 删除 | DELETE | /api/v1/shops/{id} | 删除资源 |
(2) HTTP 状态码
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 | OK | 成功响应 |
| 201 | Created | 资源创建成功 |
| 204 | No Content | 删除成功 |
| 400 | Bad Request | 请求格式错误 |
| 401 | Unauthorized | 未认证 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 422 | Unprocessable Entity | 验证失败 |
| 429 | Too Many Requests | 限流 |
| 500 | Internal Server Error | 服务器错误 |
(3) 错误响应格式
JSON
{
"success": false,
"message": "Validation failed.",
"errors": {
"name": ["The name field is required."],
"email": ["The email must be a valid email address."]
}
}
▶ 示例:ShopMetrics RESTful API 端点设计
PHP
// routes/api.php
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
// Shops
Route::apiResource('shops', Api\V1\ShopController::class);
Route::get('shops/{shop}/analytics', [Api\V1\ShopAnalyticsController::class, 'show']);
// Products (nested then shallow)
Route::apiResource('shops.products', Api\V1\ProductController::class)->shallow();
// Orders
Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show', 'update']);
Route::post('orders/{order}/cancel', [Api\V1\OrderController::class, 'cancel']);
// Categories
Route::apiResource('categories', Api\V1\CategoryController::class)->only(['index', 'show']);
// Analytics
Route::get('analytics/overview', [Api\V1\AnalyticsController::class, 'overview']);
});
输出:
TEXT
📖 仅展示
// 执行成功
6. 过滤、排序与分页封装
(1) Query Filter 基类
PHP
// app/Filters/QueryFilter.php
abstract class QueryFilter
{
public function __construct(protected Request $request) {}
public function apply(Builder $query): Builder
{
foreach ($this->filters() as $filter => $value) {
if (method_exists($this, $filter) && $value !== null) {
$this->$filter($query, $value);
}
}
return $query;
}
protected function filters(): array
{
return $this->request->all();
}
}
(2) 具体 Filter
PHP
// app/Filters/ShopFilter.php
class ShopFilter extends QueryFilter
{
public function search(Builder $query, string $value): Builder
{
return $query->where('name', 'like', "%{$value}%");
}
public function status(Builder $query, string $value): Builder
{
return $query->where('status', $value);
}
public function min_revenue(Builder $query, float $value): Builder
{
return $query->where('revenue', '>=', $value);
}
public function sort(Builder $query, string $value): Builder
{
$direction = str_starts_with($value, '-') ? 'desc' : 'asc';
$field = ltrim($value, '-');
return $query->orderBy($field, $direction);
}
}
▶ 示例:ShopMetrics 带过滤的 API 端点
PHP
// app/Http/Controllers/Api/V1/ShopController.php
class ShopController extends Controller
{
public function index(ShopFilter $filter): JsonResponse
{
$shops = Shop::where('tenant_id', tenant()->id)
->filter($filter)
->withCount(['products', 'orders'])
->paginate(request()->integer('per_page', 15));
return ShopResource::collection($shops);
}
public function store(StoreShopRequest $request): JsonResponse
{
$shop = Shop::create(array_merge($request->validated(), ['tenant_id' => tenant()->id]));
return response()->json([
'message' => 'Shop created.',
'data' => new ShopResource($shop),
], 201);
}
public function show(Shop $shop): JsonResponse
{
$shop->load(['products' => fn ($q) => $q->active()->latest()->take(10)]);
return new ShopResource($shop);
}
public function update(UpdateShopRequest $request, Shop $shop): JsonResponse
{
$shop->update($request->validated());
return new ShopResource($shop);
}
public function destroy(Shop $shop): Response
{
$shop->delete();
return response()->noContent();
}
}
输出:
TEXT
📖 仅展示
// 执行成功
7. API 版本控制
(1) 路由分组版本
PHP
// routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('shops', Api\V1\ShopController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('shops', Api\V2\ShopController::class);
});
(2) Resource 继承
PHP
// V1 ShopResource
namespace App\Http\Resources\Api\V1;
class ShopResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'status' => $this->status,
];
}
}
// V2 ShopResource — extends and adds fields
namespace App\Http\Resources\Api\V2;
class ShopResource extends \App\Http\Resources\Api\V1\ShopResource
{
public function toArray(Request $request): array
{
return array_merge(parent::toArray($request), [
'revenue' => (float) $this->revenue,
'products_count' => $this->whenCounted('products'),
'links' => [
'self' => route('api.v2.shops.show', $this->id),
],
]);
}
}
| 版本策略 | 做法 | 优缺点 |
|---|---|---|
| URL 前缀 | /api/v1/, /api/v2/ |
✅ 简单明确 |
| Header | Accept: application/vnd.api.v2+json |
更 RESTful 但复杂 |
| Query | ?version=2 |
不推荐,不 RESTful |
▶ 示例:ShopMetrics API 版本路由
PHP
// routes/api.php
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
Route::apiResource('shops', Api\V1\ShopController::class);
Route::apiResource('products', Api\V1\ProductController::class);
Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show']);
});
Route::prefix('v2')->middleware('auth:sanctum')->group(function () {
Route::apiResource('shops', Api\V2\ShopController::class);
Route::apiResource('products', Api\V2\ProductController::class);
Route::apiResource('orders', Api\V2\OrderController::class);
// V2 adds full CRUD for orders + analytics
Route::get('analytics', [Api\V2\AnalyticsController::class, 'overview']);
});
输出:
TEXT
📖 仅展示
// 执行成功
8. 综合示例:ShopMetrics API Resources 全流程
PHP
// ============================================
// Comprehensive: ShopMetrics API Resources
// Covers: resources, relations, filtering, pagination, versioning
// ============================================
// app/Http/Resources/Api/V1/OrderResource.php
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'order_number' => $this->order_number,
'status' => $this->status,
'subtotal' => (float) $this->subtotal,
'discount' => (float) $this->discount,
'total' => (float) $this->total,
'items_count' => $this->whenCounted('items'),
'items' => OrderItemResource::collection($this->whenLoaded('items')),
'shop' => new ShopBriefResource($this->whenLoaded('shop')),
'user' => [
'id' => $this->whenLoaded('user')?->id,
'name' => $this->whenLoaded('user')?->name,
],
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
];
}
}
// Controller with full pipeline
class OrderController extends Controller
{
public function index(OrderFilter $filter): JsonResponse
{
$orders = Order::where('tenant_id', tenant()->id)
->filter($filter)
->with(['shop', 'user'])
->withCount('items')
->latest()
->paginate(request()->integer('per_page', 15));
return OrderResource::collection($orders);
}
public function show(Order $order): JsonResponse
{
$order->load(['items.product', 'shop', 'user']);
return new OrderResource($order);
}
public function update(UpdateOrderRequest $request, Order $order): JsonResponse
{
$order->update($request->validated());
return new OrderResource($order->fresh()->load('items.product'));
}
}
❓ 常见问题
Q Resource 和直接返回模型 json 有什么区别?
A 直接返回模型会暴露所有字段(含密码等敏感数据),格式无法控制;Resource 精确控制输出字段、格式化值、嵌套关联。API 必须用 Resource。
Q whenLoaded 和直接访问关联有什么区别?
A 直接访问关联会触发懒加载(N+1 问题);whenLoaded 只在关联已被预加载时才输出,未加载则返回 null 并自动从 JSON 中移除。
Q API 需要多少个版本?
A 通常只维护 2 个版本(当前 + 前一个)。新版本发布时给用户 6 个月迁移期,到期后旧版本返回 410 Gone。不要同时维护太多版本。
Q ResourceCollection 分页格式可以自定义吗?
A 可以。创建自定义 Collection 类覆盖 toArray(),或使用
JsonResource::collection() 的 paginationResponse() 方法自定义分页格式。Q 如何在 Resource 中访问认证用户?
A 用
$request->user() 或 auth()->user()。Resource 的 toArray() 方法接收 Request 参数,可以基于用户权限决定输出哪些字段。Q Brief Resource 和 Full Resource 重复代码怎么办?
A 让 Brief Resource 继承 Full Resource,只覆盖 toArray() 输出更少字段;或在 Full Resource 中用
$this->when() 根据上下文动态输出字段。📖 小节
- API Resource 控制字段暴露、格式化和关联嵌套
- whenLoaded() 条件加载关联,避免 N+1 懒加载
- Brief Resource 用于嵌套场景,避免循环引用
- RESTful API 遵循 URI + HTTP 动词 + 状态码规范
- QueryFilter 封装过滤/排序逻辑,控制器保持干净
- API 版本用 URL 前缀(/v1/, /v2/),Resource 可继承复用
📝 作业
-
基础题(⭐):为 ShopMetrics 创建 ShopResource、ProductResource、OrderResource 三个 Resource 类,在控制器中替换直接返回模型,确保 JSON 格式统一。
-
进阶题(⭐⭐):实现 ShopFilter 查询过滤器,支持 search/status/min_revenue/sort 参数,在 ShopController::index 中使用,Postman 测试过滤效果。
-
挑战题(⭐⭐⭐):设计 V1 和 V2 两个 API 版本——V1 只返回基础字段,V2 额外返回 revenue/products_count/links,使用 Resource 继承实现,确保 V1 端点不破坏现有客户端。