如果你还在为每个字段写 getXxx()/setXxx(),那么 PHP 8.4 新增的 property hooks 会改变你的建模方式。这篇文章用商品类作为案例,从零搭建一个带价格校验、折扣计算和名称清洗的实体,你会发现很多样板代码可以直接删掉。
环境要求
本文所用语法来自 PHP 8.4 或更高版本。请先确认你的运行环境支持这个特性。
第一个钩子
property hooks 允许你在声明属性时直接定义读取和写入逻辑。最简单的 get 钩子长这样:
class Product
{
private string $_name;
public string $name {
get => $this->_name;
set {
$this->_name = $value;
}
}
}
get 块用 => 返回一个值,set 块通过 $value 接收即将写入的数据。
完整商品类
下面是一个用 property hooks 实现的产品实体,包含:名称清洗、价格校验、折扣约束、动态计算折后价和描述。
class Product
{
private float $_price;
private float $_discount = 0;
private string $_name;
public function __construct(
string $name,
float $price,
float $discount = 0,
) {
$this->name = $name;
$this->price = $price;
$this->discount = $discount;
}
public string $name {
get => $this->_name;
set {
$value = trim($value);
if (mb_strlen($value) < 2) {
throw new InvalidArgumentException('商品名至少需要2个字符');
}
$this->_name = $value;
}
}
public float $price {
get => $this->_price;
set {
if ($value < 0) {
throw new InvalidArgumentException('价格不能为负数');
}
$this->_price = round($value, 2);
}
}
public float $discount {
get => $this->_discount;
set {
if ($value < 0 || $value > 1) {
throw new InvalidArgumentException('折扣必须在0到1之间');
}
$this->_discount = $value;
}
}
public float $finalPrice {
get => round($this->_price * (1 - $this->_discount), 2);
}
public string $description {
get => sprintf('%s(原价:%.2f,折后价:%.2f)', $this->name, $this->price, $this->finalPrice);
}
}
注意 $finalPrice 和 $description 是只读的,它们没有 set 钩子,一旦类外访问就通过 get 钩子动态计算。你可以直接像读取普通属性一样读取:
$product = new Product('Apple Magic Mouse', 699);
echo $product->name; // Apple Magic Mouse
echo $product->price; // 699
echo $product->finalPrice; // 699
$product->discount = 0.2;
echo $product->finalPrice; // 559.2
$product->price = -1; // 抛出 InvalidArgumentException
传统写法有多啰嗦
如果不使用钩子,你大概会这样写:
class Product
{
private string $name;
private float $price;
private float $discount = 0;
public function __construct(string $name, float $price, float $discount = 0)
{
$this->setName($name);
$this->setPrice($price);
$this->setDiscount($discount);
}
public function getName(): string
{
return $this->name;
}
public function setName(string $value): void
{
$value = trim($value);
if (mb_strlen($value) < 2) {
throw new InvalidArgumentException('商品名至少需要2个字符');
}
$this->name = $value;
}
// ... 其余 getter/setter 和计算逻辑
}
同样的逻辑,传统写法至少要为每个属性写两个方法,还要在构造器里一一手动调用。property hooks 直接让属性本身拥有行为,调用方不需要记住方法名。
注意事项
1. 避免递归
在 get 钩子内部访问同一个属性会再次调用 get 钩子,导致无限递归。通常需要一个私有后备字段,如本文中的 $_name,公开属性只作为接口。
2. 钩子不支持静态属性
PHP 8.4 规定 property hooks 只能用于实例属性,static 属性不能定义钩子。
3. 序列化时的表现
带钩子的公开属性如果是虚拟的(没有后备存储),默认不会被序列化。私有后备字段会被序列化。若需要定制,请实现 __serialize()/__unserialize()。
4. 反射检测
ReflectionProperty 新增了 isVirtual() 等方法,用于判断当前属性是否带钩子或是否为虚拟属性。框架和库可以据此调整行为。
更深一层:业务规则
钩子不仅能做格式化,还能直接内联状态规则。比如一个订单类,可以阻止某些状态跳转:
class Order
{
private int $_state = 0;
public int $state {
get => $this->_state;
set {
if ($this->_state === 2 && $value !== 3) {
throw new LogicException('已完成订单只能进入退款流程');
}
$this->_state = $value;
}
}
}
当状态为2且新值不是3时,抛出异常。这比在业务方法里手写 if 判断更直观,因为状态字段本身就带有规则。
结论
Property hooks 让 PHP 的属性表达能力接近 C# 或 Kotlin。它适合那些带有校验、格式化和计算逻辑的实体类,你可以把散落各处的 getter/setter 收拢到属性声明中。只要注意递归和序列化细节,它能显著减少样板代码。值得在 PHP 8.4 项目中尝试。

