public、protected、private 在继承链中的可见性

解读

国内面试官问这道题,并不是想听“public 哪儿都能用、protected 子类能用、private 只能自己用”这种课本式背诵,而是想确认两点:

  1. 你对 PHP 继承链(单继承 + 多层级)中“可见性”的底层规则是否真正写过代码、踩过坑;
  2. 你是否知道语言级细节(Trait、魔术方法、反射、对象句柄上下文)会让“可见性”出现表面违反直觉的现象。
    回答时务必用“代码场景 + 运行结果”说话,再给出工程中的防御性写法,才能体现“干过百万级项目”的经验。

知识点

  1. 可见性三字的本质是“编译期语法检查”而非“运行期能力限制”。
    • private 只允许“定义时类”内部访问,与“对象真实类型”无关;
    • protected 允许“定义时类 + 所有子类”内部访问,但只能在“继承链内”通过 $this、parent、self 调用;
    • public 无限制。
  2. 继承链中“同名属性/方法”出现时会触发“兼容规则”:
    • 子类重写时只能扩大或维持可见性,不能缩小(private→protected→public 合法,反向 Fatal)。
  3. 对象句柄的“类作用域”由“当前行代码所在的类”决定,而不是“对象实际类型”。
    • 父类方法里访问 this>privateVar永远访问的是“父类自己那份”,即使this->privateVar 永远访问的是“父类自己那份”,即使 this 真实指向子类对象。
  4. 反射、Closure::bindTo、__get/__call 等机制可以绕过语法检查,但工程上被视为“黑魔法”,面试官会追问风险。
  5. Trait 中定义的 private/protected 在 use 到类后,可见性按“use 后的类”重新计算,可能产生“同名不同源”的隐藏属性。
  6. 从 PHP 8.0 起,private 方法不再参与子类重写检查(与 Java 类似),但属性仍按“同名即覆盖”处理,容易踩坑。

答案

先给一段最能区分“纸上谈兵”与“实战派”的代码,再逐行拆解:

<?php
class A {
    private   $foo = 'A::foo(private)';
    protected $bar = 'A::bar(protected)';
    public    $baz = 'A::baz(public)';

    public function testInsideA() {
        echo $this->foo, PHP_EOL; // ✅ 合法:A 内部访问 A 自己声明的 private
    }
}

class B extends A {
    private   $foo = 'B::foo(private)';  // 与 A::foo 同名但不同源,互不影响
    protected $bar = 'B::bar(protected)';// 覆盖父类属性
    public    $baz = 'B::baz(public)';   // 覆盖父类属性

    public function testInsideB() {
        // echo $this->foo;   // ❌ 编译期 Fatal:A 的 private 在 B 内不可见
        echo $this->bar, PHP_EOL; // ✅ 访问的是 B 自己那份 protected
        echo $this->baz, PHP_EOL; // ✅ 访问的是 B 自己那份 public
    }

    public function accessParentBar() {
        return parent::$bar ?? 'notFound'; // ❌ 语法错误:属性不能用 parent:: 访问
    }
}

class C extends B {
    public function demo() {
        // 想在 C 里访问“爷爷类 A”的 private/protected?做不到,除非反射
        $refA = new ReflectionClass('A');
        $prop = $refA->getProperty('foo');
        $prop->setAccessible(true);
        echo $prop->getValue($this), PHP_EOL; // ✅ 输出 A::foo(private)
    }
}

// ---------- 运行 ----------
$a = new A;
$a->testInsideA();      // A::foo(private)

$b = new B;
$b->testInsideB();      // B::bar(protected)  B::baz(public)

$c = new C;
$c->demo();             // A::foo(private)  通过反射绕过可见性
?>

结论一句话:
“private 只在‘声明它的类’里可见,继承链再深也看不见;protected 可以在整条继承链内部被访问,但外部(包括其他类、实例化代码)一律拒绝;public 完全开放。任何试图用‘对象实际类型’去推理可见性的思路,在 PHP 里都会翻车。”

拓展思考

  1. 工程落地:
    • 如果业务需要“子类只读父类私有状态”,用 protected getter 而不是反射,避免性能与维护灾难。
    • 对于“资金、订单”等核心实体,把属性设为 private,提供 final protected 的校验方法,既让子类能参与业务,又杜绝外部篡改。
  2. 框架源码:
    • Laravel Eloquent 模型里 $attributes 是 protected,通过 __get/__set 统一入口,既保证继承链可扩展,又避免 public 污染。
  3. 升级兼容:
    • PHP 8.0 之后 private 方法不再被视作“可重写”,老项目里若用 private 方法实现“模板方法模式”,升级前需全部改为 protected,否则会出现“静默失效”。
  4. 面试反杀:
    • 可以反问面试官:“咱们业务代码是否用反射绕过可见性?如果有,如何防止后续重构时 BC break?”——体现你对“可见性不仅是语法问题,更是架构边界”的深度理解。