在之前写用户模型的时候,我习惯把密码、余额这类敏感字段设成private,然后配上getBalance()方法暴露读取,再用setBalance()来控制写入。这种套路写多了,一堆getter和setter堆在类里,不光代码变长,新同事也很难一眼看出哪些属性是只读的、哪些是可读可写的。PHP 8.4 带来的非对称可见性(Asymmetric Visibility),直接把这个麻烦给解决了——你可以在属性声明时分别指定get和set的可见性,比如“任何人都能读,但只有自己类内部才能改”。
这比传统的private加公开方法直观得多,也不需要再用__get/__set魔术方法搞隐式访问。下面我用两个实际的业务场景,把非对称可见性的用法和好处掰开揉碎讲清楚。
老办法:用方法包装属性的无奈
拿一个电商系统中的“用户钱包”来说,余额必须由内部逻辑(充值、支付)修改,但外部需要能随时查看。过去我们这样写:
class Wallet {
private float $balance = 0.0;
public function getBalance(): float {
return $this->balance;
}
public function deposit(float $amount): void {
if ($amount balance += $amount;
}
public function pay(float $amount): void {
if ($amount > $this->balance) throw new RuntimeException('余额不足');
$this->balance -= $amount;
}
}
外部要读取余额就得调用$wallet->getBalance(),写操作则被deposit和pay两个方法拦截。看起来没啥大问题,但如果这个钱包类有十几个属性,比如冻结金额、可用额度、积分等,每个都配一个getter,整个类会膨胀得很难看。
新思路:在属性上直接声明两种可见性
PHP 8.4 允许在属性前用public、protected、private 分别修饰 get 和 set,语法是 修饰词 get 可见性 set 可见性。最常见的组合就是public get, private set:外面可以直接读,但只有类内部能改。
重写上面的钱包类:
class Wallet {
public float $balance {
get => $this->balance;
private set => $this->balance = $value;
}
public function __construct() {
$this->balance = 0.0;
}
public function deposit(float $amount): void {
if ($amount balance += $amount; // 这里可以访问private set
}
public function pay(float $amount): void {
if ($amount > $this->balance) throw new RuntimeException('余额不足');
$this->balance -= $amount;
}
}
注意到属性定义了get和set,但跟Property Hooks不同,这里的get只是直接返回,set也只是简单赋值,没有附加逻辑。关键是set被声明为private,外部任何直接对$wallet->balance = 100的尝试都会触发类型错误。这样一来,属性的读和写权限就分开了,不需要再多写一个getter。
调用方式也随之变得自然:
$wallet = new Wallet();
echo $wallet->balance; // 允许,显示 0
// $wallet->balance = 50; // 直接报致命错误,阻止了非法写入
$wallet->deposit(100);
echo $wallet->balance; // 100
混合可见性:public get, protected set
还有一种常见需求:属性允许子类和父类修改,但外部只能读。非对称可见性同样支持protected修饰set。
例如订单的“状态”属性,子类(如各种订单子类型)可能需要更新状态,但外部业务逻辑不应直接写入:
class Order {
public string $status {
get => $this->status;
protected set => $this->status = $value;
}
public function __construct() {
$this->status = 'pending';
}
}
class ExpressOrder extends Order {
public function ship(): void {
$this->status = 'shipped'; // 允许,因为set是protected
}
}
$order = new ExpressOrder();
echo $order->status; // pending
// $order->status = 'cancelled'; // 外部写入,报错
$order->ship();
echo $order->status; // shipped
这样的封装强度,靠以前private属性配合setStatus()方法也能实现,但代码清晰度差一截。非对称可见性把意图直接写在了属性定义上,阅读代码的人一眼就知道这个属性的读写规则。
配合构造器提升与只读属性
如果某个属性只需要在构造函数里赋值一次,之后就变成只读,我们可以把set声明为private并且不暴露任何修改方法。这就等同于readonly修饰符,但比readonly更灵活的地方在于,你仍然可以在类的内部方法中修改它,而readonly是在整个对象生命周期内都不允许更改。
class User {
public string $email {
get => $this->email;
private set => $this->email = $value;
}
public function __construct(string $email) {
$this->email = $email; // 构造时允许赋值
}
public function changeEmail(string $newEmail): void {
// 内部方法允许修改
$this->email = $newEmail;
}
}
如果希望某个属性只在初始化时设置一次,之后绝不可变,那还是用readonly更合适(并且readonly属性不能再配合非对称可见性,两者互斥)。
与Property Hooks的区别:别搞混了
细心的朋友会发现,前面例子里也出现了get和set的花括号语法,似乎跟Property Hooks长得一模一样。两者的关系是这样:非对称可见性是通过声明不同可见性来控制读写的权限,它建立在属性钩子(Property Hooks)的基础上。也就是说,当你需要自定义读写逻辑(比如set时加密、get时格式化),就用完整的钩子写法;如果你只需要权限区分,就可以像上面那样,用最简的get => $this->prop; set => $this->prop = $value;并附加可见性。实际上非对称可见性就是属性钩子的一个精简用例。
开发中实际受益的场景
- 值对象和DTO:对外只读,内部构造时赋值,完美契合。
- 聚合根实体:标识符(id)只能由持久化逻辑设置,但外部可以查看。
- 配置类:配置项允许读取,但变更必须通过特定方法,避免随意修改。
- 视图模型:把数据库返回的字段暴露给模板,但禁止模板里直接修改。
注意事项与局限性
- 只能在PHP 8.4及以上版本运行,低版本会报语法错误。如果项目还在用8.2或8.3,可以暂时用传统private+getter方式,并准备升级。
- 非对称可见性与
readonly互斥,一个属性不能同时标上readonly和private(set)。 - 在
var_dump或print_r对象时,这些属性仍会显示,因为get只是控制显式访问,不改变序列化行为。 - 和Property Hooks一起使用时,
set的可见性需要跟钩子的可见性协同,通常按照最严格的那个算。
总结
非对称可见性给我的最大感觉是:它让“封装”这件事重新回到了属性定义本身。以前封装要通过方法间接实现,现在直接在属性上写明权限,代码少了一层翻译。在大型项目中,这种细节累积起来的可读性提升非常可观——新人看代码时,不需要在getter和setter之间反复跳转,就能快速掌握数据流的权限边界。
如果你的PHP已经升级到8.4,不妨先把那些private属性配getXxx的类找出来,按需换成非对称可见性,这个重构的过程几乎零风险,但带来的爽快感会一直持续到下一次代码评审的时候。

