#背景
业务模型里最常见的样板代码是访问器:
final class Money
{
private int $amount = 0;
public function getAmount(): int { return $this->amount; }
public function setAmount(int $amount): void
{
if ($amount < 0) {
throw new InvalidArgumentException('金额不能为负');
}
$this->amount = $amount;
}
}一个字段配一对方法,模板代码比业务逻辑还长。属性钩子把这两件事合回属性本身。
#做法
#1. 校验直接写在属性上
final class Money
{
public int $amount = 0 {
set (int $value) {
if ($value < 0) {
throw new InvalidArgumentException('金额不能为负');
}
$this->amount = $value;
}
}
}调用侧仍然是 $money->amount = 100;,但非法值进不来。好处不只是少写代码:校验跟着字段走,新增字段时不会忘了加校验。
#2. 派生字段惰性计算
final class Post
{
public string $content_md = '';
public string $excerpt {
get => mb_substr(trim(strip_tags($this->content_md)), 0, 80);
}
}$post->excerpt 是只读派生值——不需要在每次保存时同步冗余字段,也不会有"改了正文忘了改摘要"的不一致。
#3. 与只读类配合
readonly 属性不能有 set 钩子(只能在构造器里赋值),但可以有 get 钩子做格式化。实践组合是:
- 值对象(Money、Email、Slug)用
readonly+ 构造期校验; - 可编辑模型(Post、Setting)用属性钩子做赋值校验。
#4. 与 get/set 的本质区别
魔术方法是在访问不存在的属性时兜底,它破坏了静态分析,IDE 也看不懂;属性钩子是真实属性,类型、补全、readonly、序列化都照常工作。这是它值得迁移的核心理由。
#结果
- 模型层样板代码明显减少,校验从"分散在各处 setter"变成"贴着字段";
- 静态分析(IDE 与
php -l)仍能理解属性类型,重构改字段名不会漏。
#取舍
- 不把复杂逻辑塞进钩子:钩子里只做校验与轻量派生。需要查库、发事件的逻辑仍然放在服务层——钩子会隐式触发,塞重活会让调用链难以追踪。
get钩子里不要改状态:读操作有副作用是最难排查的一类 bug,惰性计算要保证幂等。- 对外 API 仍用显式方法:跨模块的契约(如
PostService::save())保持方法调用,属性钩子只在模型内部使用——这样以后换实现不必改调用方。
评论
还没有评论,来说点什么吧。