
本文详解如何在 Laravel 的 Eloquent Resource 中安全地嵌套自关联模型(如 Category 的 parent 关系),通过 whenLoaded 避免属性访问错误、无限递归和 N+1 查询问题。
本文详解如何在 laravel 的 eloquent resource 中安全地嵌套自关联模型(如 category 的 parent 关系),通过 `whenloaded` 避免属性访问错误、无限递归和 n+1 查询问题。
在构建树形结构数据(如分类目录)的 API 时,常需将父级分类以嵌套对象形式返回,而非仅返回父 ID。但直接在 Resource 中调用 new CategoriesResource($this->parent) 会引发两个关键问题:
-
运行时错误:当
parent字段为null或整型(原始数据库值,未加载关联模型)时,$this->parent实际是int(如1),而CategoriesResource构造函数期望一个Category实例,导致Attempt to read property "id" on int错误; -
无限递归风险:若父级资源也渲染其
parent,将形成链式嵌套,最终触发内存溢出或深度超限。
✅ 正确解法是结合 关系预加载 与 Resource 条件渲染:
✅ 第一步:正确定义模型关系
确保 Category 模型中声明了自关联的 belongsTo 关系,并使用 nullable 外键约束(parent 允许为 null):
// Domain/Category/Models/Category.php
use Illuminate\Database\Eloquent\Relations\BelongsTo;
public function parent(): BelongsTo
{
return $this->belongsTo(Category::class, 'parent')->withDefault(); // withDefault() 可选,避免 null 引发异常
}
⚠️ 注意:
$fillable中不应包含parent(它应是外键字段,由关联逻辑管理),且数据库迁移中建议将parent设为unsignedBigInteger()->nullable()。
✅ 第二步:控制器中预加载关系(防 N+1)
在返回资源前,务必使用 with('parent') 预加载,否则 whenLoaded 将始终返回 null:
// Controller 示例
public function show(Category $category)
{
// 预加载 parent 关系(仅一级)
$category->load('parent');
return new CategoriesResource($category);
}
或在查询时直接链式加载:
Laravel Cloud(cloud.laravel.com)。此技能用于处理所有 Laravel Cloud 请求,包括搜索和读取数据。只要任务涉及 Laravel Cloud,即使用此技能。
$category = Category::with('parent')->findOrFail($id);
return new CategoriesResource($category);
✅ 第三步:Resource 中安全嵌套(核心修复)
使用 whenLoaded('parent') —— 它仅在 parent 关系已被加载时才执行闭包(此处为构造子资源),否则返回 null,彻底规避类型错误与递归:
// Domain/Category/Resources/CategoriesResource.php
public function toArray($request): array
{
return [
'id' => (string) $this->id,
'type' => 'categories',
'attributes' => [
'name' => $this->name,
'slug' => $this->slug,
'parent' => $this->whenLoaded('parent', fn() => new self($this->parent)),
'description' => $this->description,
'image' => $this->image,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
]
];
}
?
whenLoaded是 Laravel Resource 提供的关键工具,它内部检查$this->relationLoaded('parent'),不触发懒加载,完全依赖你主动预加载的结果,因此性能可控、行为可预测。
✅ 进阶:限制嵌套深度(可选)
若未来需支持多层(如 parent → parent → parent),推荐改用 显式深度控制 + 资源参数化,而非自动递归。例如:
// 在 Resource 构造中传入 depth 参数
public function __construct($resource, public int $depth = 1)
{
parent::__construct($resource);
}
public function toArray($request): array
{
$parentResource = null;
if ($this->depth > 0 && $this->relationLoaded('parent')) {
$parentResource = new self($this->parent, $this->depth - 1);
}
return [
// ... 其他字段
'parent' => $parentResource,
];
}
然后在控制器中调用:new CategoriesResource($category, 1)。
? 总结要点
- ❌ 禁止直接使用
$this->parent构造资源(类型不安全); - ✅ 必须配合
load('parent')或with('parent')预加载; - ✅ 必须使用
whenLoaded('parent')实现条件嵌套; - ✅ 自关联关系命名需清晰(如
parent),避免与字段名冲突; - ? 输出 JSON 符合 JSON:API 规范(如需),可进一步封装为
relationships结构。
通过以上方式,你将获得稳定、高效、可维护的嵌套分类 API,既满足前端对层级数据的需求,又坚守后端性能与健壮性底线。










