打开 Hyperf/Laravel 的 Model.php,阅读 getAttribute 和 __get 的实现。
·
它的本质是:**这两个方法构成了 Eloquent/Hyperf Model 的 数据访问网关 (Data Access Gateway)。
__get($key):是 PHP 引擎的入口钩子 (Entry Hook)。当访问不存在的属性时触发,它负责处理 关联关系 (Relations) 的特殊逻辑(如将方法调用转换为关系查询),然后委托给getAttribute。getAttribute($key):是 核心调度中心 (Central Dispatcher)。它按照严格优先级:访问器 (Accessors) > 属性值 (Attributes) > 关联关系 (Relations) 来解析数据。- 核心逻辑:别把 Model 当成简单的数组。它是一个智能代理,能在你访问属性的瞬间,决定是返回内存中的值、格式化后的值,还是去数据库查一张新表。
一、源码拆解:Laravel vs. Hyperf
虽然两者实现细节略有不同,但核心逻辑高度一致。我们以 Laravel 9/10 为例(Hyperf 类似,但基于协程和不同的 DI 容器)。
1. __get($key): 守门人
// Illuminate\Database\Eloquent\Model.php
public function __get($key)
{
// 1. 尝试获取属性(包括访问器和原始属性)
return $this->getAttribute($key);
}
- 注意:在较旧版本或特定场景下,
__get可能包含更多逻辑,但在现代 Laravel 中,它几乎直接委托给getAttribute。 - Hyperf 的差异:Hyperf 的
__get可能还处理了 协程上下文 或 缓存读取 的特殊逻辑,但大体结构相同。
2. getAttribute($key): 调度中心
public function getAttribute($key)
{
if (! $key) {
return;
}
// 1. 检查是否是 "属性名" 或 "访问器" (Accessor)
// hasAttributeGetMutator 检查是否存在 getXXXAttribute 方法
if (array_key_exists($key, $this->attributes) ||
$this->hasAttributeGetMutator($key)) {
return $this->getAttributeValue($key);
}
// 2. 检查是否是 "关联关系" (Relation)
// method_exists 检查是否有名为 $key 的方法
if (method_exists($this, $key)) {
return $this->getRelationValue($key);
}
// 3. 都不存在,抛出异常或返回 null
throw new InvalidArgumentException(sprintf(
'Undefined property [%s] on [%s].', $key, static::class
));
}
3. getAttributeValue($key): 取值与格式化
public function getAttributeValue($key)
{
// 1. 从内部 $attributes 数组获取原始值
$value = $this->getAttributeFromArray($key);
// 2. 如果有访问器 (Mutator/Accessor),调用它进行格式化
if ($this->hasAttributeGetMutator($key)) {
return $this->mutateAttribute($key, $value);
}
// 3. 如果是日期字段,转换为 Carbon 实例
if ($this->isDateAttribute($key)) {
return $this->asDateTime($value);
}
// 4. 如果是类型转换字段 (Casts),进行转换 (如 int, bool, object)
if ($this->hasCast($key)) {
return $this->castAttribute($key, $value);
}
return $value;
}
4. getRelationValue($key): 懒加载的核心
public function getRelationValue($key)
{
// 1. 如果已经加载过该关系,直接返回内存中的结果
if ($this->relationLoaded($key)) {
return $this->relations[$key];
}
// 2. 如果存在对应的方法(如 public function posts())
if (method_exists($this, $key)) {
// 3. 调用该方法,获取 Relation 对象,并执行查询
return $this->getRelationshipFromMethod($key);
}
}
二、执行流程:当你写下 $user->name 时
假设代码:$name = $user->name;
- PHP 引擎:发现
User类没有public $name属性。 - 触发
__get('name')。 - 调用
getAttribute('name')。 - 判断逻辑:
- 路径 A (普通字段):
array_key_exists('name', $this->attributes)为true(数据已从 DB 取出)。- 调用
getAttributeValue('name')。 - 检查是否有
getNameAttribute()访问器?- 有:调用访问器,返回格式化后的值。
- 无:直接返回
$this->attributes['name']。
- 路径 B (关联关系):
array_key_exists为false。method_exists($this, 'name')为false(假设没有叫 name 的方法)。- 结果:抛出异常或返回 null。
- 路径 C (关联关系 - 如
$user->posts):array_key_exists('posts', ...)为false。method_exists($this, 'posts')为true(存在public function posts()方法)。- 调用
getRelationValue('posts')。 - 检查是否已加载?否。
- 调用
$this->posts()方法,获取HasMany对象。 - 执行 SQL 查询:
SELECT * FROM posts WHERE user_id = ?。 - 将结果存入
$this->relations['posts']。 - 返回查询结果集合。
- 路径 A (普通字段):
💡 核心洞察:
__get是触发器,getAttribute是路由器,getAttributeValue是加工厂,getRelationValue是挖掘机。
三、关键差异:Laravel vs. Hyperf
| 特性 | Laravel Eloquent | Hyperf Model |
|---|---|---|
| 底层驱动 | PDO (同步阻塞) | Swoole/Swow Coroutine MySQL Client (异步非阻塞) |
| 连接管理 | 每次查询建立/复用连接 | 协程连接池 (Connection Pool),自动借还 |
| 懒加载风险 | 阻塞当前线程,性能瓶颈明显 | 协程挂起,不阻塞 Worker,但仍需注意 N+1 |
| 缓存集成 | 需手动集成或使用包 | 内置 Redis 缓存支持,可配置自动缓存查询结果 |
| 注解支持 | 主要靠方法和约定 | 支持 #[Column], #[HasMany] 等注解定义关系 |
__get 实现 |
基本一致 | 基本一致,但可能增加协程上下文检查 |
Hyperf 特别注意事项:
在 Hyperf 中,由于协程的存在,懒加载 ($user->posts) 虽然不会阻塞整个进程,但会 挂起当前协程 等待 I/O。如果在循环中懒加载,依然会产生大量的数据库往返 (RTT),导致性能下降。必须使用 with() 预加载。
四、认知牢笼:常见误区
1. 误区:“$user->name 和 $user->attributes['name'] 是一样的。”
- 真相:
$user->attributes['name']:直接取原始值,跳过 访问器、日期转换、类型转换。$user->name:经过完整处理链(访问器、Cast、Date)。- 对策:除非你需要绕过格式化逻辑,否则始终使用
->name。
2. 误区:“__get 可以访问私有方法。”
- 真相:
method_exists检查的是 所有可见性 的方法。- 如果你有一个
private function secret(),访问$user->secret会触发getRelationValue,尝试调用它,可能导致意外行为或错误。 - 对策:避免将关联关系方法与私有辅助方法同名。
3. 误区:“懒加载很方便,所以随便用。”
- 真相:
- N+1 问题:循环中访问关系会导致 N 次额外查询。
- Hyperf 中:虽然是协程,但频繁的网络 I/O 依然耗时。
- 对策:始终使用
User::with(['posts'])->get()。
4. 误区:“我可以动态添加属性 $user->foo = 'bar'。”
- 真相:
- 这会存入
$user->attributes['foo']。 - 但它 不会 持久化到数据库,除非你明确将其加入
$fillable或$guarded并保存。 - 对策:动态属性仅用于临时数据传输,不要依赖其持久性。
- 这会存入
5. 误区:“getAttribute 只读数据库。”
- 真相:
- 它首先读 内存 (
$this->attributes)。 - 只有当关系未加载时,才查数据库。
- 对策:理解内存缓存机制,避免重复查询。
- 它首先读 内存 (
🚀 总结:原子化“Model 属性访问”全景图
| 维度 | 关键点 |
|---|---|
| 本质 | 智能数据代理,统一访问接口 |
| 核心方法 | __get (入口), getAttribute (路由), getAttributeValue (加工), getRelationValue (挖掘) |
| 优先级 | Accessors > Attributes/Casts > Relations |
| 性能陷阱 | N+1 查询,魔术方法开销 |
| 最佳实践 | 预加载 (with),使用访问器格式化,避免直接操作 attributes 数组 |
| PHP 隐喻 | Concierge who decides whether to fetch from Pocket, Safe, or Warehouse |
| 公式 | Data_Access = Memory_Lookup ^ (Accessor_Transform + Relation_Query) |
终极心法:
Model 属性访问的本质,是“延迟的智慧”。
不到最后一刻,不查数据库。
但要知道,每一次访问都可能是一次昂贵的旅行。
于代理中见灵活,于懒加载中见风险;以预加载为尺,解 N+1 之牛,于 ORM 使用中,求高效之真。
行动指令:
- 断点调试:在
getAttribute方法中打断点,观察$user->name和$user->posts的执行路径差异。 - 查看 SQL 日志:开启 Hyperf/Laravel 的 SQL 日志,观察懒加载触发的查询。
- 测试访问器:创建一个
getNameAttribute方法,观察$user->name返回值的变化。 - 优化 N+1:找出项目中的一个 N+1 场景,改为
with()预加载,对比性能。 - 思维升级:记住,Model 不是数据结构,它是行为与数据的结合体。尊重它的魔法,但要控制它的代价。
更多推荐
所有评论(0)