构造器属性提升的语法糖节省了哪些代码?
解读
国内一线、二线公司面试时,这道题常作为“语法基本功”出现,考察候选人是否真正用过 PHP 8 的新特性,能否把“样板代码”与“业务代码”区分开。
很多候选人只回答“少写了赋值语句”,却忽略了可见性声明、类型声明、默认值、文档注释四个维度,导致得分不高。
面试官期待的,是“逐行对比”式的量化回答,并顺带说明可读性与维护性提升,而不是一句“省了几行”。
知识点
- 构造器属性提升(Constructor Property Promotion)
语法位置:构造器形参列表前加可见性修饰符(public / protected / private)。
生效版本:PHP 8.0+,与联合类型、nullsafe、命名参数同属“PHP 8 语法大礼包”。 - 编译阶段行为
编译器自动完成三件事:
a) 生成同名类属性;
b) 把参数值赋给该属性;
c) 继承可见性与类型声明。 - 可省略的“样板代码”
- 属性声明行
- 构造器内赋值行
- 对应 @var 注释(配合强类型可省)
- 不能提升的场景
- 接口、抽象类中无构造体;
- 属性需要复杂校验或转换;
- 需要给属性指定与参数不同的名字。
- 与只读属性(PHP 8.1 readonly)叠加
可在参数前同时写public readonly,一次性完成“声明+赋值+不可变”。
答案
以最常见的“实体类”为例,对比传统写法与提升写法,逐行量化节省。
传统写法(PHP 7.x 及 PHP 8 未使用提升):
class User
{
/** @var int */
private int $id;
/** @var string */
private string $name;
/** @var ?DateTime */
private ?DateTime $createdAt;
public function __construct(
int $id,
string $name,
?DateTime $createdAt = null
) {
$this->id = $id;
$this->name = $name;
$this->createdAt = $createdAt ?? new DateTime();
}
}
共 5 行属性声明 + 3 行赋值 + 3 行注释 = 11 行样板代码。
构造器属性提升写法:
class User
{
public function __construct(
private int $id,
private string $name,
private ?DateTime $createdAt = null,
) {
$this->createdAt ??= new DateTime();
}
}
属性声明、赋值、可见性、类型一次性完成,样板代码只剩 0 行;整个类从 18 行压缩到 8 行,节省 10 行。
若属性无需额外处理,可完全去掉构造器体,实现“零样板”:
class User
{
public function __construct(
private int $id,
private string $name,
) {}
}
此时节省:
- 2 行属性声明
- 2 行赋值
- 2 行注释
- 大括号内空体可留可不留,合计至少 6 行。
结论:
- 显性节省“属性声明 + 构造器赋值”双份代码;
- 隐性节省 @var 注释与类型重复;
- 减少命名拼写错误,提升可读性与维护性;
- 在 DTO、Entity、ValueObject 高频场景下,平均可压缩 40% 以上行数。
拓展思考
- 与只读属性结合
public readonly string $uuid;直接写在参数里,可一次性实现“注入即不可变”,避免后期手写readonly声明与赋值两行。 - 与命名参数协同
调用端new User(name: 'Tom', id: 1)无需关心参数顺序,配合提升语法,DTO 的构造器即接口,省掉 Builder 或 Setter。 - 与继承、Trait 混用注意点
若父类已声明同名 private 属性,子类提升同名参数会触发编译错误;Trait 中提升的属性冲突规则与常规属性一致,需要 resolve。 - 对代码覆盖率、静态分析的影响
提升语法生成的属性会被 PHPUnit、PHPStan、Psalm 正常识别,但老版本工具(< 0.12)可能提示“未定义属性”,升级即可。 - 面试追问方向
- “如果构造器里要对参数做校验,还能不能用提升?”
- “提升后的属性还能不能再写 set 方法?”
- “与 Java 的‘record’、Kotlin 的‘data class’相比,PHP 这种方案优缺点是什么?”
准备好这些追问,可让面试官直接给出“语法深度”高分。