toarray() 递归调用关联模型的 toarray(),自动过滤 $hidden 字段、处理 $appends,并跳过未预加载的关联(键直接消失而非为 null);$casts 和日期格式转换在此过程中生效。

toArray() 默认行为到底做了什么
它不是简单地把对象属性塞进数组,而是递归调用每个关联模型的 toArray(),同时自动过滤掉隐藏字段($hidden)、追加字段($appends)也会按规则处理。如果你没重写过 toArray(),那它底层走的是 Illuminate\Database\Eloquent\Model 的默认实现,会跳过未加载的关联关系(比如没用 with() 预加载的 user 关系),对应键直接消失,不是 null —— 这点容易误判。
常见错误现象:dd($model->toArray()) 看不到预期的关联数据,但 dd($model->user) 又能取到 —— 八成是漏了预加载。
- 必须显式调用
load()或with()才会让关联出现在toArray()结果里 -
$casts定义的类型转换(如'active' => 'boolean')会在toArray()中生效 - 日期字段默认转为
Y-m-d H:i:s格式字符串,不是Carbon实例
怎么精确控制某个字段不转数组或强制转
想让一个字段永远不出现在 toArray() 结果里?靠 $hidden 最直接;但要是只想在某些场景下隐藏,就得临时干预。反过来,有些字段默认不会被转(比如计算属性),得手动加进 $appends 并提供对应的 getXXXAttribute() 方法。
使用场景:API 返回用户信息时,要排除 password_hash,但又要带上 is_subscribed(非数据库字段)。
- 永久屏蔽:在模型里加
protected $hidden = ['password_hash']; - 动态屏蔽:用
$model->makeHidden(['api_token']),注意这会返回新实例,原模型不变 - 强制加入:定义
protected $appends = ['is_subscribed'];,再写public function getIsSubscribedAttribute() { return $this->subscriptions()->exists(); }
toArray() 和 json_encode() 直接传模型的区别
别以为 json_encode($model) 和 json_encode($model->toArray()) 效果一样 —— 前者会触发模型的 jsonSerialize(),而这个方法内部其实也调用了 toArray(),但多了一步:它会把未加载的关联设为 null(而不是直接丢掉),还会处理 DateTimeInterface 字段的序列化逻辑。所以如果你发现 JSON 输出里多了些 null 键,大概率是因为用了 json_encode($model) 且关联没预加载。
- 性能影响:两者最终都走一遍属性遍历,但
json_encode($model)多一层序列化钩子调用,差异微乎其微 - 兼容性:Laravel 9+ 对
jsonSerialize()的处理更严格,某些自定义序列化逻辑可能在toArray()里正常,在json_encode()里失效 - 推荐做法:统一用
toArray()+json_encode(),避免隐式行为干扰
嵌套关联太多时 toArray() 变慢怎么办
当模型带了 3 层以上关联(比如 post → comments → user → profile),又全用 with() 加载,toArray() 本身不慢,但前面的查询和内存占用会飙升。真正卡住的往往不是转换过程,而是 Eloquent 把所有关联对象实例化后逐个调 toArray() —— 每个对象都要走一遍访问器、追加逻辑、隐藏判断。
- 优先用
select()限制字段,避免查出整张表再丢弃 - 深层关联能不用就不用,改用 ID 映射或单独接口拉取
- 极端情况可绕过 Eloquent,用
DB::table()+get()拿原始数组,省去模型实例化开销 - 别重写
toArray()去“优化”—— 它只是个转换器,瓶颈从来不在那儿
最容易被忽略的是:你以为在优化 toArray(),其实该砍的是查询结构或缓存策略。模型转数组这一步,基本没有值得深挖的性能空间。











