构造器属性提升的语法糖节省了哪些代码?

解读

国内一线、二线公司面试时,这道题常作为“语法基本功”出现,考察候选人是否真正用过 PHP 8 的新特性,能否把“样板代码”与“业务代码”区分开。
很多候选人只回答“少写了赋值语句”,却忽略了可见性声明、类型声明、默认值、文档注释四个维度,导致得分不高。
面试官期待的,是“逐行对比”式的量化回答,并顺带说明可读性与维护性提升,而不是一句“省了几行”。

知识点

  1. 构造器属性提升(Constructor Property Promotion)
    语法位置:构造器形参列表前加可见性修饰符(public / protected / private)。
    生效版本:PHP 8.0+,与联合类型、nullsafe、命名参数同属“PHP 8 语法大礼包”。
  2. 编译阶段行为
    编译器自动完成三件事:
    a) 生成同名类属性;
    b) 把参数值赋给该属性;
    c) 继承可见性与类型声明。
  3. 可省略的“样板代码”
    • 属性声明行
    • 构造器内赋值行
    • 对应 @var 注释(配合强类型可省)
  4. 不能提升的场景
    • 接口、抽象类中无构造体;
    • 属性需要复杂校验或转换;
    • 需要给属性指定与参数不同的名字。
  5. 与只读属性(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 行。

结论:

  1. 显性节省“属性声明 + 构造器赋值”双份代码;
  2. 隐性节省 @var 注释与类型重复;
  3. 减少命名拼写错误,提升可读性与维护性;
  4. 在 DTO、Entity、ValueObject 高频场景下,平均可压缩 40% 以上行数。

拓展思考

  1. 与只读属性结合
    public readonly string $uuid; 直接写在参数里,可一次性实现“注入即不可变”,避免后期手写 readonly 声明与赋值两行。
  2. 与命名参数协同
    调用端 new User(name: 'Tom', id: 1) 无需关心参数顺序,配合提升语法,DTO 的构造器即接口,省掉 Builder 或 Setter。
  3. 与继承、Trait 混用注意点
    若父类已声明同名 private 属性,子类提升同名参数会触发编译错误;Trait 中提升的属性冲突规则与常规属性一致,需要 resolve。
  4. 对代码覆盖率、静态分析的影响
    提升语法生成的属性会被 PHPUnit、PHPStan、Psalm 正常识别,但老版本工具(< 0.12)可能提示“未定义属性”,升级即可。
  5. 面试追问方向
    • “如果构造器里要对参数做校验,还能不能用提升?”
    • “提升后的属性还能不能再写 set 方法?”
    • “与 Java 的‘record’、Kotlin 的‘data class’相比,PHP 这种方案优缺点是什么?”
      准备好这些追问,可让面试官直接给出“语法深度”高分。