ADC 后端 API 资源类使用指南
2025/11/28大约 11 分钟
ADC 后端 API 资源类使用指南
目录
强制规范
除数据流之外的所有对外 API 必须通过
TgkwAdc\Helper\ApiResponseHelper返回由 Resource 包装后的数据,禁止直接返回模型或数组!
违反此规范的代码将无法通过代码审查。
快速开始
基础用法
use TgkwAdc\Resource\BaseResource;
use TgkwAdc\Helper\ApiResponseHelper;
// 1. 定义资源类
class UserResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->formatDate($this->created_at),
];
}
}
// 2. 在控制器中使用
public function show(int $id): JsonResponse {
$user = User::findOrFail($id);
return ApiResponseHelper::success(new UserResource($user));
}
// 3. 返回集合
public function index(): JsonResponse {
$users = User::all();
return ApiResponseHelper::success(UserResource::collection($users));
}什么是资源类
Resource(资源类)负责数据转换和格式化,隔离数据层与表现层,确保 API 响应格式统一。
核心特性
| 特性 | 说明 |
|---|---|
| 灵活输入 | 支持模型、数组、集合、自定义对象等任意数据结构 |
| 标准输出 | 输出统一的 API 响应格式 |
| 中心化管理 | 集中管理数据转换逻辑,避免散落各处 |
为什么使用资源类
1. 统一 API 响应格式
问题:没有资源类时,接口返回格式不一致
// ❌ 接口 1:字段冗余
return User::find(1);
// ❌ 接口 2:命名不统一
return ['user_id' => $user['id'], 'user_name' => $user['name']];
// ❌ 接口 3:类型错误
return ['created_at' => $order->created_at]; // 返回 Carbon 对象解决:使用资源类后,格式统一管控
// ✅ 所有接口格式统一
class UserResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
'created_at' => $this->created_at->format('Y-m-d H:i:s'),
];
}
}2. 分离数据转换与业务逻辑
// ❌ 控制器混杂格式化逻辑
public function show(User $user) {
$this->authorize('view', $user);
return [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar ? asset($user->avatar) : 'default.png',
'posts_count' => $user->posts->count(),
];
}
// ✅ 职责分离
public function show(User $user) {
$this->authorize('view', $user);
return new UserResource($user); // 控制器只管业务
}3. 复用格式化逻辑
// ✅ 基础资源类(公共逻辑)
class BaseUserResource extends BaseResource {
protected function commonFields(): array {
return [
'id' => $this->id,
'name' => $this->name,
'avatar' => $this->avatar ?? 'default.png',
];
}
}
// ✅ 列表资源(复用基础类)
class UserListResource extends BaseUserResource {
public function toArray(): array {
return $this->commonFields();
}
}
// ✅ 详情资源(扩展字段)
class UserDetailResource extends BaseUserResource {
public function toArray(): array {
return array_merge($this->commonFields(), [
'email' => $this->email,
'created_at' => $this->formatDate($this->created_at),
]);
}
}4. 灵活控制数据权限
class UserResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
// 仅管理员返回 email
'email' => $this->when(Auth::user()->isAdmin(), $this->email),
// 仅详情页返回 bio
'bio' => $this->when(Request::routeIs('users.show'), $this->bio),
];
}
}5. 简化关联数据处理
class UserResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
// 嵌套资源集合,自动处理关联
'posts' => PostResource::collection($this->whenLoaded('posts')),
'role' => new RoleResource($this->whenLoaded('role')),
];
}
}6. 便于维护与版本迭代
class UserResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
'user_name' => $this->name, // 新字段名
'name' => $this->name, // 保留旧字段,兼容前端
'new_field' => $this->new_field,
// 'old_field' => $this->old_field, // 直接注释删除
];
}
}资源类分层架构
在微服务架构中,资源类分为两层:领域层(Domain) 和 应用层(Application)。
分层维度对比
| 维度 | 领域层资源类(Domain Resource) | 应用层资源类(Application Resource) |
|---|---|---|
| 维护方 | 归属服务(业务提供方) | 调用方 / 网关服务(接口提供方) |
| 面向场景 | 内部 RPC 调用、业务逻辑处理 | 对外 HTTP API、前端 / 第三方调用 |
| 字段特点 | 原始业务数据,与数据库模型强绑定 | 适配 API 规范,可格式化 / 重命名 / 组合 |
| 版本管理 | 无需区分版本(内部统一迭代) | 必须区分版本(v1/v2,兼容外部调用) |
| 核心目标 | 保证业务逻辑的纯粹性和稳定性 | 保证对外接口的兼容性和灵活性 |
微服务分层说明
| 资源类类型 | 归属服务 | 核心职责 | 示例字段 |
|---|---|---|---|
| 领域层资源类 | B 服务 | • 标准化内部核心数据模型 • 面向内部 RPC 接口 • 字段为原始核心数据 | id、title、price |
| 应用层资源类 | A 服务 | • 面向对外 API 的数据封装 • 按项目规范调整字段命名和格式 • 可组合多个领域层资源 | goods_id、status_text、formatted_price |
为什么要分层
职责清晰
- 领域层:专注业务核心数据,保证内部一致性
- 应用层:专注适配外部需求,避免外部改动影响核心业务
降低耦合
- 对外 API 字段变化不影响领域层数据结构
- 领域层改动时,只需应用层做适配
跨团队协作
- 领域层 RPC 接口是稳定契约,各团队遵守
- 应用层可自由组合转换,不必改动源服务
未来扩展
- 外部 API 新增字段,应用层自行处理
- 领域层保持纯净,避免频繁调整
调用关系示例
┌─────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│客户端│ ───> │ A 服务 │ ───> │ B 服务 │ ───> │领域层资源│ ───> │应用层资源│
│ │ │(API层) │ (RPC) │(业务层) │ │ 类返回 │ │ 类转换 │
└─────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘目录结构与命名规范
标准目录结构
app/Resource/
├── Application/ # 应用层资源类(对外 API)
│ └── V1/
│ ├── User/
│ │ ├── UserResource.php # 单个用户资源
│ │ └── UserCollection.php # 用户集合资源
│ └── Role/
│ ├── RoleResource.php
│ └── RoleCollection.php
└── Domain/ # 领域层资源类(内部 RPC)
├── User/
│ ├── UserResource.php
│ └── UserCollection.php
└── Role/
├── RoleResource.php
└── RoleCollection.php命名规范
| 类型 | 命名规则 | 示例 |
|---|---|---|
| Resource 类 | {资源名}Resource | UserResource、OrderResource |
| Collection 类 | {资源名}Collection | UserCollection、OrderCollection |
| 目录名 | 按模块分组 | User/、Role/、Order/ |
支持的输入数据类型
只要数据能通过「属性访问」($data->field)或「数组访问」($data['field'])获取字段,都可以被资源类处理。
1. 模型实例(最常用)
use TgkwAdc\Resource\BaseResource;
class UserResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->format('Y-m-d'),
];
}
}
// 使用:直接传入模型
$user = User::find(1);
return new UserResource($user);2. 数组 / 关联数组
// 模拟非模型数据
$userData = [
'id' => 100,
'name' => '测试用户',
'email' => 'test@example.com',
'age' => 25,
];
// 资源类无需修改,直接复用
return new UserResource($userData);3. 集合(模型集合 / 普通集合)
// 模型集合
$users = User::where('status', 1)->get();
return UserResource::collection($users);
// 普通集合
$collection = collect([
['id' => 1, 'name' => 'A'],
['id' => 2, 'name' => 'B'],
]);
return UserResource::collection($collection);4. 自定义对象(实现访问器)
class CustomUser {
private array $data;
public function __construct(array $data) {
$this->data = $data;
}
public function __get(string $key) {
return $this->data[$key] ?? null;
}
}
// 传入自定义对象
$customUser = new CustomUser(['id' => 200, 'name' => '自定义用户']);
return new UserResource($customUser);常用方法
BaseResource 方法
| 方法 | 说明 | 示例 |
|---|---|---|
when($condition, $value) | 条件返回字段 | $this->when($isAdmin, $this->email) |
whenLoaded($relation) | 关联数据(避免 N+1) | $this->whenLoaded('posts') |
whenPivotLoaded($relation) | 中间表数据 | $this->whenPivotLoaded('roles') |
merge($data) | 合并额外数据 | $this->merge(['extra' => 'value']) |
formatDate($date) | 格式化日期 | $this->formatDate($this->created_at) |
formatMoney($amount) | 格式化金额 | $this->formatMoney($this->price) |
getEnumText($enum) | 获取枚举文本 | $this->getEnumText($this->status) |
BaseCollection 方法
| 方法 | 说明 |
|---|---|
withData($data) | 添加额外数据 |
withMeta($meta) | 添加元数据(分页信息等) |
withStats($stats) | 添加统计信息 |
filter($callback) | 过滤集合数据 |
sort($callback) | 排序集合数据 |
代码示例
条件字段
public function toArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
// 仅管理员可见 email
'email' => $this->when(Auth::user()->isAdmin(), $this->email),
];
}关联数据处理
public function toArray(): array {
return [
'id' => $this->id,
// 单个关联
'role' => new RoleResource($this->whenLoaded('role')),
// 集合关联
'posts' => PostResource::collection($this->whenLoaded('posts')),
];
}数据转换
public function toArray(): array {
return [
'id' => $this->id,
'status_text' => $this->getEnumText($this->status),
'created_at' => $this->formatDate($this->created_at),
'price' => $this->formatMoney($this->price),
];
}实战案例
案例 1:用户列表与详情
// 应用层资源类
namespace App\Resource\Application\V1\User;
use TgkwAdc\Resource\BaseResource;
use TgkwAdc\Resource\BaseCollection;
// 列表资源(简化字段)
class UserListResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
'avatar' => $this->avatar ?? 'default.png',
'status' => $this->getEnumText($this->status),
];
}
}
// 详情资源(完整字段)
class UserDetailResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email, // 详情才返回
'phone' => $this->phone,
'avatar' => $this->avatar ?? 'default.png',
'status' => $this->getEnumText($this->status),
'created_at' => $this->formatDate($this->created_at),
'updated_at' => $this->formatDate($this->updated_at),
// 嵌套关联
'role' => new RoleResource($this->whenLoaded('role')),
'posts' => PostResource::collection($this->whenLoaded('posts')),
];
}
}
// 控制器使用
class UserController extends Controller {
public function index(): JsonResponse {
$users = User::with('role')->paginate(15);
return ApiResponseHelper::success(UserListResource::collection($users));
}
public function show(int $id): JsonResponse {
$user = User::with(['role', 'posts'])->findOrFail($id);
return ApiResponseHelper::success(new UserDetailResource($user));
}
}案例 2:分页数据处理
// 控制器
public function index(Request $request): JsonResponse {
$page = $request->input('page', 1);
$pageSize = $request->input('page_size', 15);
$users = User::query()
->with('role')
->paginate($pageSize, ['*'], 'page', $page);
// BaseCollection 自动处理分页格式
return ApiResponseHelper::success(UserResource::collection($users));
}
// 返回格式示例
{
"code": 0,
"message": "success",
"data": {
"data": [...], // 数据列表
"current_page": 1, // 当前页
"per_page": 15, // 每页数量
"total": 100, // 总数
"last_page": 7 // 总页数
}
}案例 3:多数据源聚合
// 聚合多个服务的数据
class DashboardResource extends BaseResource {
public function toArray(): array {
return [
// 用户统计
'user_stats' => [
'total' => $this->user_stats['total'] ?? 0,
'active' => $this->user_stats['active'] ?? 0,
],
// 订单统计
'order_stats' => [
'total' => $this->order_stats['total'] ?? 0,
'pending' => $this->order_stats['pending'] ?? 0,
],
// 收入统计
'revenue_stats' => [
'today' => $this->formatMoney($this->revenue_stats['today'] ?? 0),
'month' => $this->formatMoney($this->revenue_stats['month'] ?? 0),
],
];
}
}
// 控制器
public function dashboard(): JsonResponse {
$userData = UserService::getStats(); // 调用用户服务
$orderData = OrderService::getStats(); // 调用订单服务
$dashboardData = [
'user_stats' => $userData,
'order_stats' => $orderData,
];
return ApiResponseHelper::success(new DashboardResource($dashboardData));
}性能优化
1. 避免在 Resource 中进行数据库查询
// ❌ 错误:在 Resource 中查询数据库
class UserResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
'posts_count' => $this->posts()->count(), // 触发查询
];
}
}
// ✅ 正确:在控制器中预加载
class UserController extends Controller {
public function show(int $id): JsonResponse {
$user = User::withCount('posts')->findOrFail($id);
return ApiResponseHelper::success(new UserResource($user));
}
}
class UserResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
'posts_count' => $this->posts_count, // 使用预加载的计数
];
}
}2. 使用 whenLoaded 避免 N+1 查询
// ✅ 正确:使用 whenLoaded
class UserResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
// 仅在关联已加载时返回
'posts' => PostResource::collection($this->whenLoaded('posts')),
];
}
}
// 控制器根据场景决定是否加载
public function index(): JsonResponse {
$users = User::all(); // 不加载 posts
return ApiResponseHelper::success(UserResource::collection($users));
}
public function show(int $id): JsonResponse {
$user = User::with('posts')->findOrFail($id); // 加载 posts
return ApiResponseHelper::success(new UserResource($user));
}3. 合理使用缓存
class UserResource extends BaseResource {
public function toArray(): array {
return [
'id' => $this->id,
// 缓存枚举文本转换
'status_text' => Cache::remember("user.status.{$this->status}", 3600, function () {
return $this->getEnumText($this->status);
}),
];
}
}4. 优化大数据集处理
// ✅ 使用 cursor 分块处理大数据集
User::cursor()->map(function ($user) {
return new UserResource($user);
});
// ✅ 使用 chunk 分批处理
User::chunk(1000, function ($users) {
foreach ($users as $user) {
// 处理每个用户
}
});最佳实践
1. 统一格式
所有 API 通过 Resource 返回,确保格式一致:
// ✅ 统一返回格式
return ApiResponseHelper::success(new UserResource($user));
return ApiResponseHelper::success(UserResource::collection($users));2. 数据隔离
Resource 只负责数据转换,不处理业务逻辑:
// ❌ 错误:在 Resource 中处理业务逻辑
class UserResource extends BaseResource {
public function toArray(): array {
// 发送欢迎邮件(业务逻辑)
Mail::to($this->email)->send(new WelcomeMail());
return ['id' => $this->id];
}
}
// ✅ 正确:在控制器或服务中处理业务逻辑
class UserService {
public function createUser(array $data): User {
$user = User::create($data);
Mail::to($user->email)->send(new WelcomeMail()); // 业务逻辑
return $user;
}
}3. 条件显示
使用 when() 方法根据权限显示不同字段:
public function toArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->when(Auth::user()->isAdmin(), $this->email),
];
}4. 关联处理
使用 whenLoaded() 避免 N+1 查询问题:
public function toArray(): array {
return [
'posts' => PostResource::collection($this->whenLoaded('posts')),
];
}5. 性能考虑
避免在 Resource 中进行复杂计算或查询:
// ❌ 错误:复杂计算
class OrderResource extends BaseResource {
public function toArray(): array {
return [
'discount' => $this->calculateComplexDiscount(), // 复杂计算
];
}
}
// ✅ 正确:在服务中预计算
class OrderService {
public function getOrderWithDiscount(int $id): array {
$order = Order::findOrFail($id);
$order->discount = $this->calculateComplexDiscount($order);
return $order;
}
}6. 版本管理
应用层资源类按版本分目录(V1、V2),领域层无需版本区分:
app/Resource/
├── Application/
│ ├── V1/User/UserResource.php # v1 版本
│ └── V2/User/UserResource.php # v2 版本(不兼容 v1)
└── Domain/User/UserResource.php # 领域层(无版本)常见问题
Q1:Resource 和 Collection 的区别?
| 类型 | 用途 | 返回数据 |
|---|---|---|
| Resource | 单个资源 | 单个对象 { ... } |
| Collection | 资源集合 | 数组 [...] 或分页数据 |
// 单个用户
return new UserResource($user); // { "id": 1, "name": "..." }
// 用户列表
return UserResource::collection($users); // [{...}, {...}]Q2:如何处理字段不存在的情况?
// 使用 null 合并运算符提供默认值
public function toArray(): array {
return [
'id' => $this->id ?? 0,
'name' => $this->name ?? '未知',
'avatar' => $this->avatar ?? 'default.png',
];
}Q3:如何嵌套资源?
public function toArray(): array {
return [
'id' => $this->id,
// 嵌套单个资源
'role' => new RoleResource($this->whenLoaded('role')),
// 嵌套资源集合
'posts' => PostResource::collection($this->whenLoaded('posts')),
];
}Q4:分页数据如何处理?
// 控制器
$users = User::paginate(15);
// BaseCollection 自动处理分页格式
return UserResource::collection($users);Q5:如何添加额外数据?
// 方法 1:使用 merge
public function toArray(): array {
return array_merge([
'id' => $this->id,
'name' => $this->name,
], $this->extraData);
}
// 方法 2:Collection 添加额外数据
UserResource::collection($users)
->withData(['extra' => 'value'])
->withMeta(['total' => $users->total()]);Q6:应用层和领域层如何选择?
| 场景 | 使用 |
|---|---|
| 对外 HTTP API | 应用层资源类 |
| 内部 RPC 调用 | 领域层资源类 |
| 需要版本管理 | 应用层资源类 |
| 跨服务调用 | 领域层资源类 |
总结
资源类是 API 开发中数据层的核心组件,通过合理使用资源类可以:
- ✅ 统一 API 响应格式
- ✅ 分离数据转换与业务逻辑
- ✅ 提高代码复用性
- ✅ 灵活控制数据权限
- ✅ 简化关联数据处理
- ✅ 便于版本迭代和维护
遵循本文档的规范和最佳实践,可以有效提升 API 的质量和可维护性。
贡献者
ou.阳