API Resource 与 ResourceCollection
直接在控制器里 return $task->toArray() 会暴露所有数据库字段,包括密码哈希和内部字段。API Resource 是数据库模型和 API 响应之间的转换层——控制哪些字段暴露、如何格式化、如何条件性包含关联数据。
创建 Resource
php artisan make:resource TaskResource
php artisan make:resource TaskCollection # 集合 Resource(可选)
<?php
// app/Http/Resources/TaskResource.php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class TaskResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
// 基础字段
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'status' => $this->status,
'priority' => $this->priority,
// 日期格式化
'due_date' => $this->due_date?->toDateString(),
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
// 计算字段
'is_overdue' => $this->due_date && $this->due_date->isPast() && $this->status !== 'done',
// 条件性关联(只在预加载时包含,避免 N+1)
'user' => new UserResource($this->whenLoaded('user')),
'project' => new ProjectResource($this->whenLoaded('project')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
'comments' => CommentResource::collection($this->whenLoaded('comments')),
// 条件性字段(按权限或按请求上下文)
'cost' => $this->when($request->user()?->can('manage-billing'), $this->estimated_cost),
'internal_notes' => $this->when($request->user()?->hasRole('admin'), $this->internal_notes),
// 计数(withCount 后才有)
'comments_count' => $this->whenCounted('comments'),
'tags_count' => $this->whenCounted('tags'),
// 聚合(withSum、withAvg 等)
'total_time' => $this->whenAggregated('timeEntries', 'minutes', 'sum'),
// 链接(HATEOAS 风格,可选)
'links' => [
'self' => route('api.v1.tasks.show', $this->id),
],
];
}
}
集合 Resource
<?php
// app/Http/Resources/TaskCollection.php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class TaskCollection extends ResourceCollection
{
// 自定义集合的包装格式
public $collects = TaskResource::class; // 每个元素用 TaskResource 转换
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total_count' => $this->total(),
'pending_count' => $this->collection->where('status', 'pending')->count(),
'completed_count' => $this->collection->where('status', 'done')->count(),
],
];
}
}
在控制器中使用
// 单个资源
return new TaskResource($task);
// 集合(自动分页包装)
return TaskResource::collection(Task::paginate(20));
// 响应格式:
// {
// "data": [...],
// "links": { "first": ..., "last": ..., "prev": null, "next": "..." },
// "meta": { "current_page": 1, "total": 150, "per_page": 20 }
// }
// 使用自定义集合 Resource
return new TaskCollection(Task::paginate(20));
// 添加额外的响应头或状态码
return (new TaskResource($task))
->response()
->setStatusCode(201)
->header('X-Resource-Id', $task->id);
UserResource 完整示例
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'avatar' => $this->avatar_url,
// 只对管理员或用户自己显示 email_verified_at
'email_verified_at' => $this->when(
$request->user()?->id === $this->id || $request->user()?->hasRole('admin'),
$this->email_verified_at?->toISOString()
),
// 角色(只对管理员显示)
'roles' => $this->when(
$request->user()?->hasRole('admin'),
$this->getRoleNames()
),
'created_at' => $this->created_at->toISOString(),
// 关联
'tasks_count' => $this->whenCounted('tasks'),
];
}
}
全局响应包装
// 如果需要修改全局 API 响应格式(如统一加 status/message 字段)
// 在 AppServiceProvider::boot() 中:
use Illuminate\Http\Resources\Json\JsonResource;
JsonResource::withoutWrapping(); // 去掉默认的 "data" 包装
// 或者:自定义包装格式(继承 ResourceResponse)
// 一般不推荐,让前端适应标准的 Laravel Resource 格式更简单
下一节:API 版本控制策略——API 上线后,如何安全地迭代而不破坏已有客户端?URL 路径版本化是最直观的方案,结合路由分组和 Resource 类继承,可以优雅地管理 API 版本。