数字化技能提升教程,数字化时代生存宝典

Laravel API Resources

浏览:88

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 或业务字段表达的信息,不必重复包装多层。

九、验证接口输出

完成配置后,可以按以下顺序检查:

  1. 详情接口:只返回 Resource 中声明的字段。
  2. 列表接口:每条记录使用相同的字段结构。
  3. 分页结果:links 和 meta 能够正确表示当前页。
  4. 关联数据:加载分类时返回分类,没有加载时不额外查询。
  5. 条件字段:include_description 能够控制描述字段。
  6. 访问权限:未授权用户无法获取受限记录。

Resource 层的字段测试可以使用以下断言形式:

$response->assertOk()
    ->assertJsonStructure([
        'data' => [
            'id',
            'name',
            'price_cents',
            'currency',
        ],
    ])
    ->assertJsonMissingPath('data.cost_price');

其中,$response 应来自测试中对真实接口的请求。敏感字段断言应替换为项目实际需要保护的字段。

广告合作

广告合作

QQ群号:4114653

QQ群号:4114653

温馨提示:
  1. 本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。邮箱:2942802716#qq.com(#改为@)。
  2. 本站原创内容未经允许不得转载,转载请注明出处“站长百科”和原文地址。