PHP 8.4属性钩子实战:Property Hooks让Getter/Setter不再必需

2026-09-10 0 342

我从 PHP 5.3 时代就开始写业务代码,这些年最让我有点厌烦的东西,大概就是实体类里那一片片只读不改的 getter。以前为了把某个金额字段保护起来,要么加一个方法,要么用反射绕来绕去,代码越写越绕。

直到 PHP 8.4 正式把属性钩子(Property Hooks)带进来,我才觉得“属性”这两个字终于名副其实了。这篇文章我不想只列文档语法,而是直接用一个带折扣和库存预警的商品类,带你把完整的实战思路过一遍。

从 Getter 到属性钩子,差的不只是几个字母

先看一个很常见的类:商品有一个原始价格,外面读钱的时候要打八折。老写法里,你需要把“价格”藏成私有属性,然后暴露一个 getPrice() 方法。

class Product
{
    private float $originalPrice = 0;

    public function setPrice(float $price): void
    {
        if ($price <= 0) {
            throw new InvalidArgumentException('价格必须大于 0');
        }
        $this->originalPrice = $price;
    }

    public function getPrice(): float
    {
        return round($this->originalPrice * 0.8, 2);
    }
}

业务调用起来很别扭。负责渲染的同事总得问:到底哪些地方该用 getPrice(),哪些地方能直接读属性?这种心智负担叠加多了,就容易在代码里看到各种奇奇怪怪的自创方法名。

属性钩子的思路很简单:把读和写的拦截逻辑,直接挂在属性声明上。

$product->price = 100;
echo $product->price;

外部读写的姿势和普通属性一样,但计算和校验逻辑也没丢。这就是我想要的。

实战:一个商品类的价格与库存逻辑

下面我用一个带折扣的商品类做演示。需求是:

  • 外部设置价格时做正数校验,读取时自动返回折后价;
  • 折扣百分比外部只能读,不能随便改;
  • 库存扣减低于安全线时自动给出一条预警。
<?php

class Product
{
    public string $name;

    private float $originalPrice = 0;

    public private(set) float $discountPercent = 0;

    public float $price {
        get {
            return round($this->originalPrice * (1 - $this->discountPercent / 100), 2);
        }
        set(float $value) {
            if ($value <= 0) {
                throw new InvalidArgumentException('价格必须大于 0');
            }
            $this->originalPrice = $value;
        }
    }

    private int $currentStock = 0;

    private int $lowStockThreshold = 10;

    public int $stock {
        get => $this->currentStock;
        set(int $value) {
            if ($value < 0) {
                throw new InvalidArgumentException('库存不能为负数');
            }
            $this->currentStock = $value;

            if ($value < $this->lowStockThreshold) {
                printf("[库存预警] %s 当前库存只剩余 %d 件n", $this->name, $value);
            }
        }
    }

    public function __construct(string $name, float $price, int $stock = 0)
    {
        $this->name = $name;
        $this->price = $price;
        $this->stock = $stock;
    }

    public function setDiscount(float $percent): void
    {
        if ($percent < 0 || $percent > 90) {
            throw new InvalidArgumentException('折扣必须在 0 到 90 之间');
        }

        $this->discountPercent = $percent;
    }
}

其中这段是最关键的部分:

public float $price {
    get {
        return round($this->originalPrice * (1 - $this->discountPercent / 100), 2);
    }
    set(float $value) {
        if ($value <= 0) {
            throw new InvalidArgumentException('价格必须大于 0');
        }
        $this->originalPrice = $value;
    }
}

price 实际上是一个虚拟属性,它自己没有额外的存储空间,真实原价放在 originalPrice 里。读取价格时,把原价拿出来按当前折扣算一遍;设置价格时,只做校验和存储。这么写以后,任何外部代码都不需要关心“折扣后价格”是怎么算出来的。

下面是使用效果:

$product = new Product('机械键盘', 399, 50);
$product->setDiscount(20);

echo $product->price, PHP_EOL;           // 319.2
echo $product->discountPercent, PHP_EOL; // 20
echo $product->stock, PHP_EOL;           // 50

try {
    $product->discountPercent = 30;
} catch (Error $e) {
    echo '不能直接改折扣: ', $e->getMessage(), PHP_EOL;
}

$product->stock = 5; // 库存低于 10,触发预警

一个容易绕晕的细节

property hooks 刚发布时,社区里不少人第一反应是:能不能在 set 钩子里直接写 $this->sameProperty = $value?其实这里要看你声明的属性是一个有后备存储的属性,还是一个纯虚拟属性。

如果属性本身没有后备存储,你在钩子里再对同一个名字赋值,等待你的只会是运行时错误或者意想不到的递归。即便语法上允许某些情况下的自赋值,这种隐式行为也很难读。

所以我更推荐上面的做法:把真实状态放到一个独立的私有属性里,让有钩子的属性保持“虚拟”状态。这样既不会踩递归坑,代码逻辑也更直白。等以后大家都习惯了后备字段的细节,再考虑更紧凑的写法也不迟。

不对称可见性:订单优惠码更省心

PHP 8.4 的另一个特性“不对称可见性”和属性钩子特别搭,它就是用来解决“外部能读但不能写”这个老问题的。

public private(set) float $discountPercent = 0;

意思是这个属性任何人都可以读,但是只有类内部才能写。外部想改折扣,只能调 setDiscount() 方法。

再举一个订单场景。每个订单只能绑定一次优惠码,绑定之后如果需要释放,也只能走内部方法。

class Order
{
    public string $orderNo;

    public private(set) ?string $couponCode = null;

    public function __construct(string $orderNo)
    {
        $this->orderNo = $orderNo;
    }

    public function applyCoupon(string $code): void
    {
        if ($this->couponCode !== null) {
            throw new LogicException('这个订单已经绑定了优惠码');
        }

        // 这里可以接上真实的优惠券核销逻辑
        $this->couponCode = $code;
    }

    public function releaseCoupon(): void
    {
        $this->couponCode = null;
    }
}

$order = new Order('D20250715001');
echo $order->couponCode ?? '暂无优惠码', PHP_EOL;

$order->applyCoupon('SAVE50');
echo $order->couponCode, PHP_EOL;

$order->releaseCoupon();
echo $order->couponCode ?? '优惠码已释放', PHP_EOL;

以前要做到同样的效果,我得写一个私有属性再加一个公开 getter。现在一行 public private(set) 就解决了。

什么场景不建议用属性钩子

属性钩子不是万能钥匙。如果一个 get 钩子里要查数据库、调外部接口、做复杂缓存,那我建议你还是老老实实写方法。

钩子的调用频率通常比方法高,而且其他地方读属性时不会意识到背后有开销。一旦把重逻辑塞进去,排查性能问题的时候会有点崩溃。

另外,老项目如果重度依赖 PHPStan、Psalm 或者其他静态分析工具,要先确认版本是否支持 8.4 的钩子语法。目前不少工具能解析了,但旧版仍可能存在误报。

小结

PHP 8.4 的属性钩子和不对称可见性,说到底是把原来散落在方法里的样板逻辑收拢回属性本身。它没有改变 PHP 的本质,只是让代码更贴近人的直觉。

如果你最近正好在做一个 DTO、一个实体类,又或者被一堆 getter 搞得很烦躁,可以试着把其中几个改造成属性钩子,感受一下这种“读属性就像读字段”的体验。对我来说,这是这几年 PHP 版本迭代里最让人觉得痛快的一个变化。

PHP 8.4属性钩子实战:Property Hooks让Getter/Setter不再必需
收藏 (0) 打赏

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

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

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

淘吗网 php PHP 8.4属性钩子实战:Property Hooks让Getter/Setter不再必需 https://www.taomawang.com/server/php/2737.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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