Eloquent 的 $casts 仅做类型转换而不校验合法性,如 'price' => 'int' 遇 "abc" 静默转为 0;应使用 set* 访问器或 booted() 中 setAttributeUsing 统一拦截非法值,并配合异常中断流程,确保模型层数据一致性。

Eloquent 的 $casts 和 get*/set* 访问器本身不校验数据合法性,所谓“Attribute Constraints”不是 Laravel 内置概念——得靠手动拦截或扩展实现。
为什么不能直接用 $casts 做约束?
$casts 只负责类型转换,不拒绝非法值。比如 'price' => 'int' 遇到 "abc" 会转成 0,而不是报错。
- 转换失败时静默降级(如字符串转整型失败 →
0或null) - 无法表达业务规则(如“价格必须 > 0”、“邮箱必须含 @”)
- 数据库写入前已丢失原始输入,没法回溯错误来源
在 set* 访问器里做校验最直接
把校验逻辑塞进 setFooAttribute,能在赋值第一时间拦截非法值,配合异常可中断流程。
例如限制 status 只能是预定义枚举值:
立即学习“PHP免费学习笔记(深入)”;
public function setStatusAttribute($value)
{
$allowed = ['draft', 'published', 'archived'];
if (!in_array($value, $allowed)) {
throw new InvalidArgumentException("Invalid status: {$value}");
}
$this->attributes['status'] = $value;
}
- 注意:异常要抛出(别只
return或dd()),否则调用方收不到失败信号 - 避免在访问器里做耗时操作(如查库、HTTP 请求),会拖慢所有模型赋值
- 若需兼容批量赋值(
create()/fill()),得确保校验不依赖未初始化的属性
用 booted() + setAttribute() 拦截更通用
如果不想每个字段都写 set*,可在 booted() 中重写 setAttribute(),统一处理带约束的字段:
protected static function booted()
{
static::setAttributeUsing('price', function ($value) {
if (!is_numeric($value) || (float) $value <= 0) {
throw new InvalidArgumentException('Price must be a positive number');
}
return (float) $value;
});
}
-
setAttributeUsing()是 Laravel 9.2+ 引入的机制,优先级高于$casts,但低于显式set*访问器 - 适合字段多、规则相似的场景(如所有金额字段都要求 > 0)
- 注意:该方法对
update(['price' => 'abc'])有效,但对$model->price = 'abc'无效(因为绕过了动态 setter)
和表单验证器(validate())分工要清楚
Eloquent 层校验 ≠ 请求层校验。前者保模型内部一致性,后者保输入可信度。
- 请求进来先过
validate()(防恶意/格式错),再进模型逻辑 - 模型层校验补漏:比如两个字段联动(
end_date必须 ≥start_date),或关联其他模型状态 - 别把复杂规则全堆在模型里——像“用户余额是否足够扣款”这种,应放在 Service 层,避免模型变重
真正难的不是加几行 if,而是判断哪条规则该在哪一层拦:太浅(只在 request)漏掉模型间约束,太深(全塞进 model)会让测试和复用变得笨重。



















