PHP 8.4属性钩子实战:告别setter/getter,把数据规则直接写进属性里

2026-09-02 0 926

上周把项目从PHP 8.2升级到PHP 8.4,本来只是冲着性能提升去的。结果用上属性钩子(Property Hooks)的第二天,我默默删掉了原来十几个DTO类里的一大堆getter和setter。这东西不是糖,它直接把类的设计方式给改了。

今天不念文档,直接拿一个真实场景来说透。我们要做一个订单明细类,里面有价格、数量、折扣,还得算总价,而且总价不能被外人随意修改。但最重要的是,我要让代码看起来像普通属性,但内部会自动校验和计算。

老写法:getter/setter堆成山

以前我们写一个订单明细类,大概是这个味儿:

class OrderItem
{
    private float $price;
    private int $quantity;
    private float $discount = 0.0;
    private float $total;

    public function __construct(float $price, int $quantity, float $discount = 0.0)
    {
        $this->setPrice($price);
        $this->setQuantity($quantity);
        $this->setDiscount($discount);
    }

    public function getPrice(): float
    {
        return $this->price;
    }

    public function setPrice(float $price): void
    {
        if ($price price = $price;
        $this->recalculateTotal();
    }

    public function getQuantity(): int
    {
        return $this->quantity;
    }

    public function setQuantity(int $quantity): void
    {
        if ($quantity quantity = $quantity;
        $this->recalculateTotal();
    }

    public function getDiscount(): float
    {
        return $this->discount;
    }

    public function setDiscount(float $discount): void
    {
        if ($discount  1) {
            throw new InvalidArgumentException('折扣必须在0到1之间');
        }
        $this->discount = $discount;
        $this->recalculateTotal();
    }

    public function getTotal(): float
    {
        return $this->total;
    }

    private function recalculateTotal(): void
    {
        $this->total = $this->price * $this->quantity * (1 - $this->discount);
    }
}

用起来倒是不复杂,但每个字段都要写两个方法,重复代码多,而且一眼看过去全是样板。尤其是当你有一堆DTO类的时候,真的想骂人。最烦的是,属性一变必须在setter里手动调用recalculateTotal,忘记一次就出bug。

属性钩子(Property Hooks)到底是啥?

PHP 8.4允许你像声明属性一样声明钩子逻辑。你不再需要写单独的getXxx()和setXxx(),而是直接在属性后面定义钩子。

钩子有两种:get钩子set钩子

get钩子在你读取属性时执行,set钩子在属性赋值时执行。这么一来,属性本身就变成了有逻辑的入口。

看一个最简单的例子:

class User
{
    public string $name {
        get => strtoupper($this->name);
    }
}

如果没有属性钩子,你读$user->name,直接拿到原始值。加了get钩子后,读到的就是大写的名字。赋值仍然正常,只是读取时被加工了。

实战:用属性钩子重构订单明细类

我把刚才那个OrderItem用属性钩子重写一遍,你感受一下差异。

class OrderItem
{
    public float $price {
        set {
            if ($value < 0) {
                throw new InvalidArgumentException('价格不能为负数');
            }
            $this->price = $value;
        }
    }

    public int $quantity {
        set {
            if ($value < 1) {
                throw new InvalidArgumentException('数量至少为1');
            }
            $this->quantity = $value;
        }
    }

    public float $discount {
        set {
            if ($value  1) {
                throw new InvalidArgumentException('折扣必须在0到1之间');
            }
            $this->discount = $value;
        }
    }

    public float $total {
        get => $this->price * $this->quantity * (1 - $this->discount);
    }

    public function __construct(
        float $price,
        int $quantity,
        float $discount = 0.0,
    ) {
        $this->price = $price;
        $this->quantity = $quantity;
        $this->discount = $discount;
    }
}

注意几个变化:

1. price、quantity、discount变成了public属性,但通过set钩子保证数据合法性。外部赋非法值时照样抛异常,和以前setter一样。

2. total变成了只读计算属性,它只有一个get钩子,在每次读取时动态计算结果。你再也不用担心忘记更新total了,因为它根本不存在存储值。

3. 构造函数里直接对三个属性赋值,触发了对应的set钩子,所以校验自动生效。也没有所谓“构造时别给我调setter”的别扭感。

不对称可见性:让外部只读,内部可写

订单ID、创建时间这些字段,从业务上一般不允许外部修改。以前的写法是把属性设为private,只提供getter。现在可以用不对称可见性配合属性钩子,让属性本身对外只读,对内可写。

class Order
{
    public private(set) string $id;
    public private(set) DateTimeImmutable $createdAt;

    public function __construct()
    {
        $this->id = uniqid('ord_', true);
        $this->createdAt = new DateTimeImmutable();
    }
}

这个语法public private(set)意思是:读属性时是public,任何人都能读;写属性时是private,只有类内部方法(包括构造方法)可以赋值。这个特性就是PHP 8.4的不对称可见性。

你可能会说:“以前我写private+getter,不也一样吗?” 对,但代码量少了,语义也更清楚了。你一眼就能看出id是只读属性,不需要再去看有没有setter。

钩子里的特殊变量:$value和$this

set钩子会有一个特殊的变量$value表示要赋的值。get钩子不需要参数,但可以通过$this访问其他属性。

有个陷阱就是,在set钩子内部使用$this->price = $value时,会再次触发set钩子,导致无限递归。所以PHP规定,在set钩子内赋值给同一个属性必须直接操作存储变量。看上面代码,我写的是$this->price = $value会不会递归?其实不会,因为PHP在set钩子内部对这个属性赋值时,会直接写入真实存储区域,而不会再次调用钩子。放心用。但为了更清晰,你也可以用专门的$this->price这样写。

get钩子内部如果直接写$this->total也会递归,所以我用了$this->price这些别的属性。如果你在get钩子里访问被钩子修饰的属性,需要注意。

钩子带来的更多可能性:延迟加载和缓存

get钩子不仅是计算属性,还能做懒加载。比如一个用户对象,有些字段(比如“已下单单数”)从数据库里查很费劲,只有真正用到的时候才去查,查一次之后缓存住。

class User
{
    private ?int $orderCountCache = null;

    public int $orderCount {
        get {
            if ($this->orderCountCache === null) {
                $this->orderCountCache = $this->fetchOrderCountFromDb();
            }
            return $this->orderCountCache;
        }
    }
}

这样业务代码里直接$user->orderCount,内部懒加载并缓存,细节全被封装了。

属性钩子和接口、抽象类怎么搭配?

钩子还可以在接口和抽象类中定义。比如定义一个有速率限制的接口:

interface RateLimitable
{
    public int $rateLimit { get; }
}

class ApiClient implements RateLimitable
{
    public private(set) int $rateLimit = 100;
}

我们在实现接口时,只需确保属性的公共读取能力一致即可。写起来很自然,比要求实现getRateLimit()方法更简洁。

实际升级中遇到的坑

再好的特性也会有点小坑。我这几天实战下来,遇到三个问题,提醒大家注意。

1. 不能被钩子修饰的属性不能直接初始化? 其实可以。有钩子的属性,只要没有set钩子,你是没办法从外部赋值的。但可以在构造函数里赋值。对于对称可见性,如果写了set钩子,那么外部赋值会经过set钩子。没问题。

2. 性能问题。属性钩子本质上还是方法调用,和以前直接访问public属性比,稍微慢一点。但正常业务开发完全可忽略。如果你在做极致性能优化的大循环里,频繁访问计算属性可能带来开销。这时候可以把计算结果缓存到一个中间变量。

3. 和现有代码的兼容性。这个最头疼。你的项目里有些类用的是getXxx()/setXxx()方法,现在改成属性钩子以后,所有外部调用代码都要改。比如$orderItem->getTotal()变成$orderItem->total。如果项目里引用很多,建议先用IDE全局替换,再跑一遍测试。没有测试你敢改?反正我是不敢,好在有单元测试兜底。

既然这么爽,以后怎么写类?

我个人是越来越喜欢“属性私有明明,但通过钩子暴露规则”的写法。原来一个DTO类写三四十行getter/setter,现在十行搞定,而且看起来像数据类,不是结构体。

下面这个对比很直观:

// 老写法
$item->setPrice(29.9);
$item->setQuantity(3);
$item->setDiscount(0.1);
$total = $item->getTotal();

// 新写法
$item->price = 29.9;
$item->quantity = 3;
$item->discount = 0.1;
$total = $item->total;

看起来没什么大不了,但这就是自然语言。业务代码可读性提高很多,也更符合直觉。

总结:该用还是得用,但别无脑

如果你正在新项目里使用PHP 8.4,那不用纠结,大胆使用属性钩子。特别是那些纯DTO、实体类、配置类,用上之后代码少一半。但如果你维护的是老代码,兼容性考虑可能要保守一点。

最后送上一句:别被“这是新语法”吓住。它就是给属性加个门卫,只是这个门卫比setter简洁、直白。用了就回不去了。

PHP 8.4属性钩子实战:告别setter/getter,把数据规则直接写进属性里
收藏 (0) 打赏

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

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

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

淘吗网 php PHP 8.4属性钩子实战:告别setter/getter,把数据规则直接写进属性里 https://www.taomawang.com/server/php/2696.html

常见问题

相关文章

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

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