PHP 8.4属性钩子实战:我的Model类终于不用写一堆getter和setter了

2026-08-20 0 756

做了五六年PHP,最烦的就是写那种只有几个字段的DTO或者Model。本来一个属性声明一两行就完事,结果为了封装,还得在旁边补一个getter和一个setter。代码看着凑数一样,全是重复劳动。前两天我把项目升到PHP 8.4,用了一下新出的“属性钩子”功能,突然发现以前那种憋屈的写法彻底能扔掉了。

属性钩子,真不是C#专属

如果你写过C#,对属性钩子一定不陌生。PHP 8.4也把这个语法加进来了,允许你在属性声明时直接定义读取和写入的逻辑。不需要额外写方法,直接在属性后面加一对花括号,里面写getset代码块。

先来一个最直白的例子。比如我以前要写一个用户类,属性有用户名和邮箱,通常得这么写:

class User
{
    private string $username;
    private string $email;

    public function getUsername(): string
    {
        return $this->username;
    }

    public function setUsername(string $username): void
    {
        $this->username = $username;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setEmail(string $email): void
    {
        $this->email = $email;
    }
}

这还只是两个字段,要是来个十来个字段,光这些方法就能占屏幕半页。说难听点,这代码放十年前和现在没有任何区别,纯纯体力活。

用属性钩子改写

在PHP 8.4里,上面的类可以这样写:

class User
{
    public string $username {
        get => $this->username;
        set => $value;
    }

    public string $email {
        get => $this->email;
        set => $value;
    }
}

看一眼就明白,get 后面直接写返回值,set 后面写对$value的处理。这个$value不是凭空出来的,它就是外部赋值给你的值。比如$user->email = 'foo'时,set钩子的$value就是'foo'

很多人说这也没什么区别啊?其实关键在于,你可以把处理逻辑直接内联进属性里,而不是散落在别的方法中。而且这还是强类型约束的,比写一堆方法更直观。

实战:处理密码自动哈希

我第一个真正用属性钩子的地方,就是用户模型里的密码字段。以前是这样的:

class User
{
    private string $passwordHash;

    public function setPassword(string $plainPassword)
    {
        $this->passwordHash = password_hash($plainPassword, PASSWORD_DEFAULT);
    }

    public function getPasswordHash(): string
    {
        return $this->passwordHash;
    }
}

现在用属性钩子,我能把密码加密和读取写在同一个属性上:

class User
{
    public string $password {
        set => $this->password = password_hash($value, PASSWORD_DEFAULT);
        get => $this->password;
    }
}

等等,这里有个问题。如果直接在set里给$this->password赋值,又会触发set钩子,造成无限递归。所以正确的写法是使用一个不支持钩子的底层属性来存储真实值。在PHP 8.4中,你可以在钩子内使用$this->password来写吗?实际上不行,会掉进循环。因此通常我改成这样:

class User
{
    private string $password;

    public string $passwordHash {
        get => $this->password;
        set => $this->password = password_hash($value, PASSWORD_DEFAULT);
    }
}

注意,我定义了一个私有的$password,它就是真正的存储字段。对外的$passwordHash属性只是一个接口,负责加密和暴露。这样外部可以这样用:

$user->passwordHash = 'my_plain_password'; // 自动加密存储
echo $user->passwordHash; // 输出加密后的哈希

如果你想保持命名为$password,也可以底层用其他私有属性,比如$passwordDigest。总之,不要让自己调用自己。

另一个实用场景:格式化日期

以前模型里存一个时间戳,输出时要格式化成年月日。传统写法要写一个getCreatedAtFormatted()方法,或者在模板里用date()。现在属性钩子可以直接在get里做手脚:

class Article
{
    public int $createdAt {
        get => date('Y-m-d H:i:s', $this->createdAt);
        set => $value;
    }
}

但这样有点bug,因为get返回的类型是string,而属性类型是int,类型不一致会报错。所以更合理的做法是属性命名为time,但钩子暴露的视图叫name?其实PHP允许在同一个属性上可以同时设置类型和钩子,但钩子的返回值必须兼容声明的属性类型。如果不兼容,就需要在钩子中用assert或者强制转换。我实际项目里更倾向于保留一个原始值属性,然后用一个只读的钩子属性来派生格式化结果。

class Article
{
    private int $createdTimestamp;

    public string $createdAt {
        get => date('Y-m-d', $this->createdTimestamp);
        set => strtotime($value); // 但set返回类型是int,属性是string,冲突
    }
}

这解决不了。实际上,如果你想用int存储,但对外暴露格式化字符串,最清晰的是定义两个属性:一个存数字,一个显示字符串。比如:

class Article
{
    private int $createTime;

    public string $formattedDate {
        get => date('Y-m-d', $this->createTime);
        set => $this->createTime = strtotime($value);
    }
}

这里$formattedDate的set接受字符串(比如”2025-03-20″),自动转成时间戳存在私有属性$createTime里。get则把时间戳格式化成字符串返回。完美。

和readonly在一起会怎样

有人可能想到,属性钩子能不能和readonly一起用?PHP 8.4规定不能。因为readonly属性只能初始化一次,而钩子允许set多次,语义冲突。如果你需要“外部只读,内部可改”,用我之前文章讲过的非对称可见性更合适。属性钩子适合需要对读写逻辑进行加工的场合,而不是进行权限控制。

注意钩子里的类型陷阱

我在写的时候被坑了一次。当属性声明了严格类型,比如public int $count,但set钩子返回了一个字符串,如果没开strict_types,PHP会尝试自动转换;开了strict_types,就会直接抛TypeError。为了安全,最好在set钩子里自己加上类型检查和转换。

public int $count {
        set => $this->count = (int)$value;
        get => $this->count;
}

虽然看着啰嗦,但至少不会出意外。如果你只想简单地赋值,什么都不写也行,PHP默认行为是直接存取。但那样钩子就没意义了。

将钩子用于规范化数据:邮箱小写

最实在的一个用途,估计就是自动格式化字段。比如用户注册时邮箱统一转小写:

class User
{
    private string $email = '';

    public string $emailAddress {
        get => $this->email;
        set => strtolower($value);
    }
}

由于set返回值会被赋给属性,所以字符串会转为小写。但这里有个细节:set钩子的返回值类型必须与属性类型兼容。所以如果你写成set => strtolower($value),它会用strtolower的返回值来赋值给$emailAddress,从而触发set钩子吗?不会,因为钩子本身已经结束,赋值动作是针对底层存储的,不再触发set钩子。等等,那底层的$email是怎么被赋值的?实际上在这个写法里,set钩子里的表达式”strtolower($value)”就是最终存进属性里的值,不会递归。但如果你在set钩子内部又写了$this->emailAddress = xxx,那就会递归。上面这个写法是安全的,直接返回计算结果作为最终值。

我测试过,这个写法可以。但如果你在set钩子块里写了多条语句,注意使用return关键字返回最终值。上面的箭头语法直接返回表达式,是最简写法。

更复杂的例子:一个带懒加载属性的模型

有一类需求是:某个属性值从数据库取出后,在访问时才需要额外计算或加载。比如用户的名字,存储时是JSON,读取时自动解码数组。

class UserProfile
{
    private string $metaJson;

    public array $meta {
        get => json_decode($this->metaJson, true);
        set => json_encode($value);
    }
}

这样外部直接操作数组,提交时自动转为JSON存起来。以前你还得写一个setMeta()方法,在方法里调用json_encode,现在都不用了。

值得注意的内置函数和性能

属性钩子每次访问属性都会执行对应代码,所以如果get钩子里有复杂的计算,性能肯定不如直接访问私有属性。但如果只是返回一个变量,开销几乎可以忽略。所以不用怕,正常写业务数据模型完全OK。

如果你需要在一个类里动态设置属性,可能用到__get__set,但那是动态属性,和静态声明的属性钩子不是一回事。PHP 8.4也明确动态属性被废弃了(除了某个类加了#[AllowDynamicProperties])。这是一个大趋势:类属性应该明确声明。

实际项目改造的一点感受

我把一个订单类的十几个getter/setter全部改成了属性钩子,代码量下降了四成。原来一个订单类有400行,现在只有240行,而且我甚至不用看方法名,直接看属性就知道这个字段是否可读写,可读时是不是有格式化。读代码的体验好了不少。

有一点要注意:钩子函数的访问权限和属性权限一致。如果你声明public string $name { get; set; },那么内部也可以直接访问$name属性。但如果钩子里使用外部变量,必须通过use或者全局,但这和普通方法一样,通常不建议。

总结

PHP 8.4的属性钩子是个补票功能,让PHP越来越像现代语言了。它解决的是长期依赖方法的样板代码问题。当然它也有边界:不能和readonly用,不能和static属性一起用。但对于大多数数据对象来说,这是一个相当劲的增强。

有人说“这跟宏一样,只是语法糖”,我不这么认为。它让你的类结构更清晰,把数据存取逻辑封装到了属性本身,而不是藏在任意一个setter方法中。这是一个真正的语义变化。

现在你再给我一个字段,让我加getter/setter,我会直接甩给他一对花括号。

PHP 8.4属性钩子实战:我的Model类终于不用写一堆getter和setter了
收藏 (0) 打赏

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

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

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

淘吗网 php PHP 8.4属性钩子实战:我的Model类终于不用写一堆getter和setter了 https://www.taomawang.com/server/php/2579.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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