
本文讲解如何在 laravel 的 eloquent resource 中安全地渲染自关联(如分类父子关系)的嵌套数据,避免 n+1 查询、无限递归和属性访问错误。
本文讲解如何在 laravel 的 eloquent resource 中安全地渲染自关联(如分类父子关系)的嵌套数据,避免 n+1 查询、无限递归和属性访问错误。
在构建 API 时,常需将具有自关联关系的数据(如 Category 拥有 parent_id 指向同类型模型)以嵌套 JSON 结构返回。但直接在 Resource 中递归调用自身资源极易引发两个关键问题:运行时错误(如 Attempt to read property "id" on int)和无限嵌套循环(父 → 父的父 → 父的父的父…)。
根本原因在于:你当前的 Category 模型中,parent 字段既是数据库列(整型 ID),又被定义为 Eloquent 关系方法。当未显式加载该关系时,$this->parent 实际返回的是数据库原始值(如 int 1),而非 Category 实例——此时调用 new CategoriesResource($this->parent) 就会因传入整数而触发 Cannot read property "id" 错误。
✅ 正确解法是 显式预加载 + 条件性资源包装:
首先,在控制器或查询处预加载 parent 关系(避免 N+1):
Laravel Cloud(cloud.laravel.com)。此技能用于处理所有 Laravel Cloud 请求,包括搜索和读取数据。只要任务涉及 Laravel Cloud,即使用此技能。
use Domain\Category\Models\Category;
use Domain\Category\Resources\CategoriesResource;
$categories = Category::with('parent')->get();
return CategoriesResource::collection($categories);
其次,在 CategoriesResource 的 toArray() 方法中,使用 Laravel Resource 提供的 whenLoaded() 辅助方法——它仅在关系已被加载(即 $this->parent 是 Category 实例)时才执行资源构造,否则返回 null:
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 CategoriesResource($this->parent)),
'description' => $this->description,
'image' => $this->image,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
]
];
}
⚠️ 重要注意事项:
-
绝不递归加载深层关系:
with('parent.parent.parent')或在parent资源中再次嵌套parent会导致无限循环。whenLoaded()本身不触发加载,仅做安全判断,因此天然阻断递归。 -
模型关系定义必须正确:确保
Category模型中的parent()方法使用belongsTo()并指定外键字段(默认为parent_id;若字段名为parent,需显式声明):public function parent(): BelongsTo { return $this->belongsTo(Category::class, 'parent'); // 显式指定外键名 } -
空值处理友好:
whenLoaded()在parent为null(根分类)时自动跳过,输出"parent": null,符合 JSON:API 规范且无需额外判空。
通过此方案,你将获得结构清晰、性能可控、无递归风险的嵌套响应,同时保持代码健壮性与可维护性。










