PHP 8.4 属性钩子实战:干掉 getter/setter,代码瘦了三倍

2026-09-05 0 272

前阵子翻旧项目里的购物车类,自己都看笑了。一个 CartItem,光是 getQuantity、setQuantity、getUnitPrice、setUnitPrice 就占了几十行,里面的参数校验几乎一模一样。最难受的是,类里真正的业务逻辑早被这些样板代码淹没,看半天不知道这个对象到底想表达什么。

后来切到 PHP 8.4,用 property hooks(属性钩子)把这个类重写了一遍,类整体短了差不多一半。于是想把这个过程完整写下来,因为我自己第一次看钩子时,也被“set 里为什么要给自身赋值”这个问题卡住过。

先从旧代码说起

这是很常见的写法:quantity 和 unitPrice 都被做成 private,靠一堆 getter/setter 保护起来,然后在小计金额上再补一个 getter。

class CartItem
{
    private int $quantity;
    private float $unitPrice;

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

    public function setQuantity(int $quantity): void
    {
        if ($quantity < 1) {
            throw new InvalidArgumentException('数量最少是 1');
        }
        $this->quantity = $quantity;
    }

    public function getUnitPrice(): float
    {
        return $this->unitPrice;
    }

    public function setUnitPrice(float $unitPrice): void
    {
        if ($unitPrice < 0) {
            throw new InvalidArgumentException('单价不能是负数');
        }
        $this->unitPrice = $unitPrice;
    }

    public function getSubtotal(): float
    {
        return $this->quantity * $this->unitPrice;
    }
}

这段代码没毛病,但问题也很明显:quantity 的赋值边界和 unitPrice 的赋值边界,是两套长得几乎一样的方法。以后想加个“数量超过 100 要二次确认”的逻辑,你得先进到这堆方法里找到对应位置,然后小心翼翼改。
属性钩子想干的事很简单:把读写时要做的事,直接放到属性声明旁边。

用属性钩子重构

下面是同一个类在 PHP 8.4 下的写法:

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

    public float $unitPrice {
        set {
            if ($value < 0) {
                throw new InvalidArgumentException('单价不能是负数');
            }
            $this->unitPrice = $value;
        }
    }

    public float $subtotal {
        get => $this->quantity * $this->unitPrice;
    }

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

先别急着说“看不懂”,其实语法很直白:set 块里有一个固定的变量 $value,它代表外部赋值时的表达式结果。你在 set 块里写完校验之后,再把这个值“落”到属性背后,赋值才算真正完成。

很多人第一次看到 $this->quantity = $value 会以为它在递归调用自己,接着脑补一场死循环。我一开始也这么想。

关键点在这里:在钩子的代码块内部写 $this->quantity,访问的不是“带钩子的属性本身”,而是属性背后那块最原始的存储空间(backing store)。PHP 特意让钩子内部访问绕过了钩子逻辑,所以不会递归。

如果你不在 set 块里主动把 $value 写回 backing store,那这次赋值会直接消失,外部赋过去的值不会对对象产生任何影响。

public string $nickname {
    set {
        // 忘记写 $this->nickname = $value;
        // 外部给 $user->nickname 赋值后,等于没赋
    }
}

上面这个坑不看文档确实容易踩,我也是在 PHP 8.4 刚出时试运行,发现属性读出来还是 null,才回头去查的说明。

虚拟属性让“小计”变得非常自然

重构后的 CartItem 里,$subtotal 是最让我喜欢的一段:它只有 get,没有 set,是一个“虚拟属性”。

所谓“虚拟”,就是它背后没有真实的数据槽位,每次读取都按 get 里的表达式现算。由于没写 set,你要是手贱写一句 $item->subtotal = 100,PHP 会直接报错,非常安全。

public float $subtotal {
    get => $this->quantity * $this->unitPrice;
}

传统的 getSubtotal() 方法当然也能做到同样的事,但属性写法把“这是一个由其他属性推导出来的结果”这件事表现得更加明确:读它像读一个普通字段,行为却像一个只读方法。

在旧代码里,你需要去方法列表里找 getSubtotal 才能理解“小计是现算的”;新代码里,你只要看一眼 subtotal 上面的三行声明,就全明白了。

什么场景最适合用属性钩子

经过这次重构,我的感受是:以下两种情况收益最大。

第一种是属性在赋值前必须做统一校验。比如金额不能为负、标题必须去空格、枚举必须落在白名单里。以前这些校验分散在 setter 里,现在有了 set 钩子,校验就写在属性身上,不会被放到类文件的其他位置。

第二种是需要实时计算的聚合结果。购物车小计、订单总金额、用户头像地址拼接,这些“看一眼就会算出来的值”适合做成只有 get 的虚拟属性。相比直接用方法,属性钩子对下游调用方更省事:调用方不需要知道它到底是内存字段还是计算结果。

但别把昂贵操作伪装成属性

不推荐把所有 getter 都改成钩子。特别是当一个 getter 后面藏着数据库查询、缓存读取、RPC 请求时,我会老老实实保留方法。

比如加载一个用户的完整档案,写成 $order->loadCustomer() 比写成 $order->customer 要好。原因是:属性这个语法本身会给读代码的人一种“访问成本不高”的直觉。如果哪天真有人在一个循环里访问了你的 $order->customer,然后发现每次触发一次远程调用,那时候追查起来会相当酸爽。

换句话说,属性钩子更适合表达“轻量逻辑”:校验、格式化、归一化、简单推导。重量逻辑还是留给方法吧。

另外有一点也要留意:PHP 8.4 的属性钩子目前和 readonly 属性之间限制不少,想在一个只读属性上做动态校验仍然需要构造函数来完成,不能指望 set 钩子去碰 readonly。

重构后的样子

最终那个购物车类,代码量从原来的老长一段变成下面这个清爽结构。去掉注释后,核心逻辑非常容易扫完:

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

    public float $unitPrice {
        set {
            if ($value < 0) {
                throw new InvalidArgumentException('单价不能是负数');
            }
            $this->unitPrice = $value;
        }
    }

    public float $subtotal {
        get => $this->quantity * $this->unitPrice;
    }

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

少了大概 40% 的样板代码,多出来的全是之前被 getter/setter 夹住的业务规则。我看完后很直接的想法是:以后写这种领域小类,应该优先考虑属性钩子,让校验和推导逻辑回到它们真正该待的地方。

PHP 8.4 属性钩子实战:干掉 getter/setter,代码瘦了三倍
收藏 (0) 打赏

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

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

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

淘吗网 php PHP 8.4 属性钩子实战:干掉 getter/setter,代码瘦了三倍 https://www.taomawang.com/server/php/2702.html

常见问题

相关文章

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

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