public、protected、private 在继承链中的可见性
解读
国内面试官问这道题,并不是想听“public 哪儿都能用、protected 子类能用、private 只能自己用”这种课本式背诵,而是想确认两点:
- 你对 PHP 继承链(单继承 + 多层级)中“可见性”的底层规则是否真正写过代码、踩过坑;
- 你是否知道语言级细节(Trait、魔术方法、反射、对象句柄上下文)会让“可见性”出现表面违反直觉的现象。
回答时务必用“代码场景 + 运行结果”说话,再给出工程中的防御性写法,才能体现“干过百万级项目”的经验。
知识点
- 可见性三字的本质是“编译期语法检查”而非“运行期能力限制”。
- private 只允许“定义时类”内部访问,与“对象真实类型”无关;
- protected 允许“定义时类 + 所有子类”内部访问,但只能在“继承链内”通过 $this、parent、self 调用;
- public 无限制。
- 继承链中“同名属性/方法”出现时会触发“兼容规则”:
- 子类重写时只能扩大或维持可见性,不能缩小(private→protected→public 合法,反向 Fatal)。
- 对象句柄的“类作用域”由“当前行代码所在的类”决定,而不是“对象实际类型”。
- 父类方法里访问 this 真实指向子类对象。
- 反射、Closure::bindTo、__get/__call 等机制可以绕过语法检查,但工程上被视为“黑魔法”,面试官会追问风险。
- Trait 中定义的 private/protected 在 use 到类后,可见性按“use 后的类”重新计算,可能产生“同名不同源”的隐藏属性。
- 从 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 里都会翻车。”
拓展思考
- 工程落地:
- 如果业务需要“子类只读父类私有状态”,用 protected getter 而不是反射,避免性能与维护灾难。
- 对于“资金、订单”等核心实体,把属性设为 private,提供 final protected 的校验方法,既让子类能参与业务,又杜绝外部篡改。
- 框架源码:
- Laravel Eloquent 模型里 $attributes 是 protected,通过 __get/__set 统一入口,既保证继承链可扩展,又避免 public 污染。
- 升级兼容:
- PHP 8.0 之后 private 方法不再被视作“可重写”,老项目里若用 private 方法实现“模板方法模式”,升级前需全部改为 protected,否则会出现“静默失效”。
- 面试反杀:
- 可以反问面试官:“咱们业务代码是否用反射绕过可见性?如果有,如何防止后续重构时 BC break?”——体现你对“可见性不仅是语法问题,更是架构边界”的深度理解。