它的本质是:**这两个方法构成了 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;

  1. PHP 引擎:发现 User 类没有 public $name 属性。
  2. 触发 __get('name')
  3. 调用 getAttribute('name')
  4. 判断逻辑
    • 路径 A (普通字段)
      • array_key_exists('name', $this->attributes)true(数据已从 DB 取出)。
      • 调用 getAttributeValue('name')
      • 检查是否有 getNameAttribute() 访问器?
        • :调用访问器,返回格式化后的值。
        • :直接返回 $this->attributes['name']
    • 路径 B (关联关系)
      • array_key_existsfalse
      • 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']
      • 返回查询结果集合。

💡 核心洞察__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 使用中,求高效之真。

行动指令

  1. 断点调试:在 getAttribute 方法中打断点,观察 $user->name$user->posts 的执行路径差异。
  2. 查看 SQL 日志:开启 Hyperf/Laravel 的 SQL 日志,观察懒加载触发的查询。
  3. 测试访问器:创建一个 getNameAttribute 方法,观察 $user->name 返回值的变化。
  4. 优化 N+1:找出项目中的一个 N+1 场景,改为 with() 预加载,对比性能。
  5. 思维升级:记住,Model 不是数据结构,它是行为与数据的结合体。尊重它的魔法,但要控制它的代价。

更多推荐