PHP 8.4不对称可见性实战:告别setter,让属性只读但内部可写

2026-08-19 0 473

用 PHP 写数据对象最烦的是什么?十个属性有八个要getter,还有两三个需要修改状态。你写了public属性又怕外部随意改动,于是退回到private加getter。PHP 8.4 出了一个新特性叫“不对称可见性”,终于解决了这个尴尬:属性可以公开读取,但只允许在类内部写入。今天拿一个订单实体的例子来整明白。

以前是怎么做的

一个订单对象,创建后订单号、用户ID、金额不应该被修改,但状态需要可以在内部变。传统写法可能是这样:

class Order {
    private string $orderNo;
    private int $userId;
    private float $amount;
    private string $status;

    public function __construct(string $orderNo, int $userId, float $amount) {
        $this->orderNo = $orderNo;
        $this->userId = $userId;
        $this->amount = $amount;
        $this->status = 'pending';
    }

    public function getOrderNo(): string { return $this->orderNo; }
    public function getUserId(): int { return $this->userId; }
    public function getAmount(): float { return $this->amount; }
    public function getStatus(): string { return $this->status; }

    public function markPaid() {
        $this->status = 'paid';
    }
}

代码不算复杂,但一旦字段变多,那一排getter看了就让人烦躁。更难受的是,如果需要很多只读字段,你只能写很多getter来暴露它们,而没法直接访问属性。

不对称可见性是什么

PHP 8.4 允许你给属性的读取和写入分别设置不同的可见性。写法是在属性声明时,增加一个括号,里面写写入的可见性。比如:

public private(set) string $orderNo;

这代表 orderNo 属性可以在任何地方被读取,但只能在类内部(private)被写入。相当于外部只能 get,不能 set。

更通俗点说:从外部看,它就像只读的public属性,但在内部,它就是普通属性,可以直接赋值。

改写上面的订单类

用了不对称可见性,上面的代码可以精简成这样:

class Order {
    public private(set) string $orderNo;
    public private(set) int $userId;
    public private(set) float $amount;
    public private(set) string $status = 'pending';

    public function __construct(string $orderNo, int $userId, float $amount) {
        $this->orderNo = $orderNo;
        $this->userId = $userId;
        $this->amount = $amount;
    }

    public function markPaid() {
        $this->status = 'paid';
    }
}

现在,外部代码可以直接读取订单号:

$order = new Order('A1001', 42, 299.00);
echo $order->orderNo;   // A1001
// $order->orderNo = 'X'; // 这行会抛 Error,因为外部不能写

同时,类内部方法 markPaid() 可以修改 status,因为写入权限是 private,类内允许。其他字段根本不需要setter,一旦创建就不可变,非常安全。

“public private(set)” 到底怎么读

很多教程会写成 public private(set) string $name,其实意思是:读取可见性为 public,写入可见性为 private。语法上,你还可以写成 protected private(set),表示外部无法读取,只能在类内部读取和写入,这其实和 private 属性没区别。更常用的还有 public protected(set),表示属性外部能读,子类能写,外部不能写。

readonly 的区别

PHP 8.2 开始有 readonly 类,但 readonly 属性只能初始化一次,之后完全不能改。对于需要“一次性初始化,之后不变”的场景够用。但有时你希望部分字段不可变,而另一部分字段需要内部动态更新。比如状态、更新时间等。

不对称可见性更细粒度,它可以让你做到:有些字段完全不变,有些字段只能内部变。而 readonly 则是全变或全不变。

举个例子,一个订单类,订单号和用户ID创建后不变,但状态可能会从 pending 变成 paid。用 readonly 不行,因为状态需要变。用不对称可见性就完美:给订单号和用户ID用 public private(set),给状态用 public private(set) 也允许在内部更新,或者直接 public 让外部随便改(当然不推荐)。

实战:实现一个不可变又灵活的用户值对象

再看个用户场景。用户注册后,邮箱和用户名不能改,但密码可以内部哈希更新,头像允许外部修改(这是一个业务规则示例)。用不对称可见性可以这样设计:

class User {
    public private(set) string $username;
    public private(set) string $email;
    public private(set) string $passwordHash;
    public protected(set) string $avatar = 'default.png';

    public function __construct(string $username, string $email, string $plainPassword) {
        $this->username = $username;
        $this->email = $email;
        $this->setPassword($plainPassword);
    }

    // 内部方法可以修改密码
    public function setPassword(string $newPassword) {
        $this->passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);
    }

    // 子类可以修改头像(protected set)
}

在这里,外部可以读取用户名和邮箱,但不能修改。密码虽然是 private set,但通过公有的 setPassword 方法修改,而不是直接操作属性。这样保持了封装,又少了 getter 的冗余。

继承和多态下的表现

如果子类想扩大写入可见性,是不允许的。比如父类属性是 public private(set),子类不能把它变成 public protected(set),因为可见性只能更严格,不能更宽松。反过来,如果父类是 public protected(set),子类可以改成 public private(set),更严格。这符合Liskov替换原则。

坑点:不能和 static 一起用

不对称可见性目前只适用于实例属性,不能用于静态属性。官方文档明确说了静态属性不支持这个语法,如果你写了 public static private(set) int $counter 会报语法错误。静态属性还是老老实实把访问器和修改器写成方法吧。

构造器里的初始化

在构造器里给这些属性赋值是没问题的,因为构造器本身就属于类内部。即使属性定义成 private(set),类内方法照样可以写。这也是它和 readonly 的区别:readonly 属性只能在声明时或构造器里赋值一次,而 private(set) 属性可以在类内任意方法中多次赋值。

反射支持

我测试了反射,ReflectionProperty 可以获取到属性的读写可见性。比如 getReadVisibility()getWriteVisibility(),但这两个方法目前还是独立的。如果你在做依赖注入容器或者框架,得注意一下,但日常业务通常用不上。

最终完整示例

<?php
class Order {
    public private(set) string $orderNo;
    public private(set) int $userId;
    public private(set) float $amount;
    public private(set) string $status = 'pending';

    public function __construct(string $orderNo, int $userId, float $amount) {
        $this->orderNo = $orderNo;
        $this->userId = $userId;
        $this->amount = $amount;
    }

    public function markPaid() {
        $this->status = 'paid';
    }

    public function refund() {
        $this->status = 'refunded';
    }
}

$order = new Order('ORD-2025-001', 88, 199.9);
echo $order->orderNo . "n";
echo $order->status . "n"; // pending
$order->markPaid();
echo $order->status . "n"; // paid

// $order->orderNo = 'X'; // 错误:Cannot access private property
// $order->status = 'cancelled'; // 错误:外部无法写
?>

这段代码跑在 PHP 8.4 上,输出正常。

更实际的场景:DTO 与查询参数

现在我在做 API 时,喜欢用这种语法定义请求 DTO。比如创建订单的请求对象:

class CreateOrderDTO {
    public private(set) string $productId;
    public private(set) int $quantity;
    public private(set) string $couponCode = '';

    public function __construct(array $data) {
        $this->productId = $data['product_id'];
        $this->quantity = (int)$data['quantity'];
        if (!empty($data['coupon_code'])) {
            $this->couponCode = $data['coupon_code'];
        }
    }
}

这样外部就不能篡改 DTO 中的数据,保证进入业务层的数据是稳定的。如果某个字段需要计算或加工,就通过内部方法完成,外部只读。

小结

PHP 8.4 的不对称可见性不是颠覆性的功能,但它让代码更简洁,表达意图更明确。以前你要么牺牲封装把属性设 public,要么写一堆 getter。现在你可以把“只读”这个语义直接写进属性定义里,类里面也保留了写入的自由。这个特性特别适合值对象、DTO、配置类等场景,我觉得它比 readonly 类更实用。如果你已经升级到 PHP 8.4,不妨试着把项目里那些只有 getter 的类改一改。

PHP 8.4不对称可见性实战:告别setter,让属性只读但内部可写
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 php PHP 8.4不对称可见性实战:告别setter,让属性只读但内部可写 https://www.taomawang.com/server/php/2568.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务