PHP 8.4 属性钩子实战:手写一个不会出错的金额对象

2026-08-13 0 750

用过 Java 或 C# 的哥们儿都知道,类里头的字段通常不能直接公开,得靠 getter 和 setter 去遮遮掩掩。PHP 之前也是这套路,动不动就写一堆 getXxx()setXxx(),真要嫌麻烦还得用魔术方法 __get __set。但那玩意儿性能差,而且类型约束也做不到很干净。

PHP 8.4 终于把 Property Hooks属性钩子)给带进来了。这玩意儿不是简单的语法糖,它能让属性本身具备行为,逻辑内聚,代码读起来就像直接读一个公开字段。今儿我不讲一堆理论,直接拿一个咱们平时天天会碰到的“金额”来练手。

先说痛点

以前咱们写个金额类,最常见的是这样:

class Money
{
    private int $amount;
    private string $currency;

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

    public function getAmount(): int
    {
        return $this->amount;
    }

    public function setAmount(int $amount): void
    {
        if ($amount < 0) {
            throw new InvalidArgumentException('金额不能为负');
        }
        $this->amount = $amount;
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }
}

小项目这么写还行,可一旦字段多起来,全是样板代码。而且调用侧还得用方法,读起来不痛快:$money->getAmount(),捏着鼻子认了。

现在有了属性钩子,咱们能把 amount 直接暴露出去,但暴露的同时还能强制校验和格式化。语法上看,属性长得像公共字段,实则背后藏着逻辑。

Property Hooks 的关键概念

简单说,属性钩子就是在一个属性声明后面用大括号挂上 getset 处理块。你可以在 set 里对赋值做校验,在 get 里返回动态计算的结果。有个特别重要的点:get 钩子的内部用 $value 代表要设置的值,而在 get 钩子里用 $this->property 会触发递归。 所以要想在钩子内部访问原始值,得用 PHP 8.4 提供的 __get 后备字段?其实,真正的做法是给属性加一个私有后备字段,但不需要手动定义,直接利用“无钩子属性”作为存储。

说白了,你要是想存一个真实值,就不能让钩子属性自己存数据。得搞一个私有的真实属性,再来一个公开的“访问口”用钩子接上去。举个例子:

class Money
{
    public int $amount {
        get {
            return $this->rawAmount;
        }
        set {
            if ($value < 0) {
                throw new InvalidArgumentException('金额不能为负');
            }
            $this->rawAmount = $value;
        }
    }

    private int $rawAmount;
}

在这个例子里,外部读 $money->amount 会走 get 钩子,内部返回 rawAmount。外部写 $money->amount = 100 会走 set 钩子先判断再存到 rawAmount。这样数据实际存在 rawAmount 这个私有的、没有钩子的属性里,安全又干净。

金额对象需求与设计

咱们要做的金额对象满足以下几个条件:

  • 金额不能是负数,否则抛异常。
  • 币种固定三位大写字母(USD、CNY 这种),赋值时自动转大写。
  • 可以方便地比较两个金额是否相等。
  • 可以返回格式化的字符串,比如 “¥100” 还是 “$100”。
  • 币种一旦创建不允许修改(不可变对象)。

用 PHP 8.4 的“不对称可见性”加上属性钩子,能省不少事。

不对称可见性的意思:这个属性可读范围是 public,但可写范围是 private。意思就是外部能读取,但是赋值只能从类内部赋。比如:

public private(set) string $currency { ... }

“public private(set)”就是不对称可见性,读是公共的,写是私有的。咱们正好拿它来做币种字段,外部只能读,不能乱改。

动手实现

直接看代码:

<?php

final class Money
{
    public private(set) string $currency {
        get {
            return $this->rawCurrency;
        }
        set {
            $this->rawCurrency = strtoupper($value);
        }
    }

    public int $amount {
        get {
            return $this->rawAmount;
        }
        set {
            if ($value < 0) {
                throw new InvalidArgumentException('金额不能为负数');
            }
            $this->rawAmount = $value;
        }
    }

    private int $rawAmount;
    private string $rawCurrency;

    public function __construct(int $amount, string $currency)
    {
        // 利用属性钩子的 set 逻辑
        $this->currency = $currency;
        $this->amount = $amount;
    }

    public function equals(Money $other): bool
    {
        return $this->amount === $other->amount
            && $this->currency === $other->currency;
    }

    public function formatted(): string
    {
        $symbol = match ($this->currency) {
            'USD' => '$',
            'CNY' => '¥',
            default => $this->currency . ' ',
        };

        return $symbol . number_format($this->amount / 100, 2);
    }

    public function __toString(): string
    {
        return $this->formatted();
    }
}

这里我把金额以“分”为单位存成整数,免得浮点数出现0.1+0.2的尴尬。构造时传的是分,比如 new Money(1000, 'usd') 表示10.00美元。币种用小写传入也没事,set钩子会给它转成大写。

看看怎么用:

$price = new Money(1000, 'usd');

echo $price->currency;      // 输出 "USD"
echo $price->amount;        // 输出 1000
echo $price->formatted();   // 输出 "$10.00"

// 试着改币种,能不能行?
// $price->currency = 'EUR'; // Fatal error: Uncaught Error: Cannot modify private(set) property Money::$currency

// 金额给负数?
// $price->amount = -5; // InvalidArgumentException: 金额不能为负数

对外看起来,$price->currency 就像个普通公共属性一样。但其实它既有格式转换(自动大写)又有访问控制(只读)。这不比 getter/setter 香?

为什么说这套写法不容易出错

以前想搞不可变金额,你得把属性全写成 private,再手动只写 getter,构造里校验。现在不对称可见性直接从语法层面保证“外部不能改”,读的时候还是公开的,连方法都省了。

而且属性钩子的校验逻辑就贴在属性旁边,下次有人要改这个类,一眼就能看到金额不能为负,不怕他把校验写在别处绕过。

那天我重构一个老项目,把一堆 setter 全换成这种写法,删了快两百行 boilerplate。看着清爽,心里也踏实。

注意几个坑

第一:钩子属性不能同时有默认值和构造赋值冲突。 如果你在构造函数里赋值,钩子会执行,OK。但如果你把属性定义成 public int $amount = 100,再写钩子,这个默认值只在实例化时赋进去。如果没走 set,没问题,但钩子的 set 可能不会在默认值时被触发。这个要小心。更稳妥的做法是全部初始化都走构造。

第二: get 钩子里访问备用字段一定不要用带钩子的属性名。 比如在 get 钩子里写 return $this->amount,会无限递归。必须用另一个私有无钩子属性来存。所以上面代码里我用 rawAmountrawCurrency,这是规则。

第三:性能开销基本可忽略。 毕竟钩子就是普通方法调用,虽然比直接读字段慢一点,但这换来的是可维护性和内聚,非常值。

顺手再玩点花活

属性钩子还能做派生属性。比如给金额类加一个“大额金额”字段,当金额超过 10000 分(即100元)时返回 true,不用单独存储字段,直接在 get 钩子里算:

class Money
{
    // ... 其他代码

    public bool $isBig {
        get {
            return $this->amount >= 10000;
        }
    }
}

然后就能这样:

$money = new Money(10000, 'USD');
var_dump($money->isBig); // bool(true)

这玩意用来做只读的计算字段真的方便,又不用缓存,又不用同步更新。

对比一下传统写法

之前写 getter 那种方式,所有上下文都得用方法,加了语法噪音。现在大部分情况直接读属性,特别符合直觉。这里不是完全否定 getter,有些重逻辑的 getter 还是保留方法好,但像取值、格式化、校验这一类,属性钩子明显更顺手。

我给大家放个对比图(用文字表示):

传统:

$money->getAmount();
$money->setAmount(50);
$money->getCurrency();

现在:

$money->amount;
$money->amount = 50;
$money->currency;

再看保护性,传统 setter 里写校验很常见。现在 set 钩子做校验,错不了。

那这玩意儿到底稳定吗

PHP 8.4 是 2024 年 11 月正式发布的,属性钩子是其中最重要的新特性之一。目前从 PHP 8.4 开始这个特性是稳定的,官方文档也给了完整说明。要是你还在用 8.3 或更早版本,那确实用不了,只能羡慕。

生产环境能不能用?我觉得如果项目已经上了 PHP 8.4,那大胆用。如果还在 8.2,那就先看看能不能升级。毕竟这种新东西早用早舒服。

不过说实话,PHP 这语言越来越讲究了。以前老被嘲“草台班子”,现在东西也算越来越现代了。

写在最后

我这篇文章没有什么大道理,就是想给兄弟们看看属性钩子到底怎么落地。你平时要写 VO、DTO、ORM Entity,这东西几乎就是催化剂。但是注意别上脑,有些地方属性钩子不适合,比如特别复杂的业务逻辑,还是得老老实实写成服务。

如果你也想试试,可以直接把上面那坨代码放到你的 PHP 8.4 环境里跑一跑,感受一下直接读属性还带校验的爽感。从此不用再写一堆枯燥的 getter/setter 了。

行了,今儿就聊到这。下次有机会我再拿属性钩子做一个完整的小型交易系统,绝对真实带感。

PHP 8.4 属性钩子实战:手写一个不会出错的金额对象
收藏 (0) 打赏

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

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

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

淘吗网 php PHP 8.4 属性钩子实战:手写一个不会出错的金额对象 https://www.taomawang.com/server/php/2535.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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