__get 仅在访问未定义的 public 属性或无对应访问器的属性时触发,读数据库字段、$casts、已定义访问器均不触发;日志需含主键、类名、关系加载状态及请求id,并限频采样。

什么时候 __get 会被触发,日志才真正开始记录
只有当访问模型上**不存在的 public 属性**,或该属性未被显式定义(比如没在 $casts、$appends 或访问器里声明),Laravel 才会走 __get。直接读 $model->name(name 是数据库字段)不会触发——它走的是 Eloquent 的原始属性获取逻辑,绕过魔幻方法。
所以想靠 __get 拦所有属性访问来记日志,天然漏掉大部分真实读取场景。真正能捕获的,其实是类似 $model->full_name(你写了 getFullNameAttribute)、或者误写的 $model->non_existent_field 这类调用。
- ✅ 正确触发日志:访问自定义访问器(
getXXXAttribute)、或未定义属性(且没被$hidden/$visible干预) - ❌ 不触发日志:读数据库字段原值、读
$casts转换后的值、读$appends字段但已定义对应访问器(此时走访问器函数,不进__get) - ⚠️ 注意:
__get在序列化(toArray()、json_encode())时也会被大量调用,容易刷爆日志
在 __get 里写日志,为什么不能只 log 属性名
光记 $key 没用。你不知道这次访问来自哪行代码、哪个请求、是否在循环里反复调用、返回值是 null 还是真实数据——这些信息缺失,日志就只剩噪音。
- 必须带上
debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)截取调用栈,至少拿到文件+行号 - 要判断是否在 API 响应上下文(比如
request()->is('api/*')),避免 Web 页面渲染时把模板层的冗余访问也记进来 - 建议加开关控制:用配置项
logging.model_attribute.enabled控制是否启用,别在生产环境默认开着 - 别用
Log::info()直写,封装成带采样率的门控日志,比如每 100 次只记 1 次:rand(1, 100) === 1
getAttribute 和 __get 日志行为差异大不大
非常大。getAttribute 是 Eloquent 内部核心方法,所有属性访问最终都会流经它(包括字段、访问器、关系)。而 __get 只是它的兜底。想全覆盖,得重写 getAttribute,但这是危险操作。
- ✅ 安全做法:在基类模型里 override
getAttribute,只对非关系、非原始字段、且非空字符串的 key 记录日志 - ❌ 危险做法:直接 patch
Illuminate\Database\Eloquent\Model,升级 Laravel 时大概率崩 - ⚠️ 关键区别:
getAttribute('name')会查数据库字段;getAttribute('name_attribute')会找getNameAttributeAttribute方法;两者日志上下文完全不同,需分开判断 - 性能影响:
getAttribute每次模型赋值都调,高频接口里加日志可能拖慢 5–10ms,务必加条件过滤(如仅限 debug 模式 or 特定用户 ID)
日志内容里漏掉这三项,基本等于白记
属性访问日志不是为了证明“有人读了”,而是为了定位“谁在什么上下文里、为什么读、读到了什么”。少一项,排查时就得来回猜。
- 必须含:
$this->getKey()(模型主键),否则分不清是 User#123 还是 User#456 - 必须含:
get_class($this)(模型类名),避免多态关联时搞混Post和Comment - 必须含:
optional($this->relationLoaded($key))->count()或类似判断,确认当前访问是否触发了 N+1(比如$post->comments首次访问关系) - 额外建议:在日志里打上 request ID(
request()->id() ?? 'cli'),方便和慢查询日志、异常日志对齐时间线
属性访问本身不耗资源,但日志 IO 和上下文采集会。最常被忽略的是:没限制日志频率,结果一个列表页拉 50 条模型,每条触发 3 个访问器,瞬间生成 150 条日志——磁盘写满前,你根本看不到关键那条。











