Laravel: Laravel API Resources与RESTful API开发

最后更新:2026-08-26

API Resources 是 Laravel 的"数据包装器"——控制哪些字段暴露给客户端、如何格式化、关联怎么嵌套,API 输出从此统一规范。

1. 你将学到


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() 根据上下文动态输出字段。

📖 小节


📝 作业

  1. 基础题(⭐):为 ShopMetrics 创建 ShopResource、ProductResource、OrderResource 三个 Resource 类,在控制器中替换直接返回模型,确保 JSON 格式统一。

  2. 进阶题(⭐⭐):实现 ShopFilter 查询过滤器,支持 search/status/min_revenue/sort 参数,在 ShopController::index 中使用,Postman 测试过滤效果。

  3. 挑战题(⭐⭐⭐):设计 V1 和 V2 两个 API 版本——V1 只返回基础字段,V2 额外返回 revenue/products_count/links,使用 Resource 继承实现,确保 V1 端点不破坏现有客户端。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏