1. 起因
项目从 PHP 7.4 升级到 8.1 之后,把之前压着没用的几个新语法都实际用了一遍,记一下体验和几个不算坑但容易搞混的地方。
2. 命名参数,可选参数一多就顺手
1 | function createUser(string $name, string $email, bool $isAdmin = false, bool $sendWelcomeEmail = true) { |
以前遇到函数签名里有一串 bool 类型的可选参数,调用的地方经常得写一堆没意义的 false, false, true 去占位,命名参数之后这类调用可读性提升明显。唯一要注意的是参数名相当于变成了公开 API 的一部分,如果一个函数是给外部调用的公共方法,改参数名就是 breaking change,这点以前不用太在意,现在得留心。
3. match 表达式,比 switch 严格也更简洁
1 | $status = match ($code) { |
跟 switch 相比有两个实际区别:一是 match 内部比较用的是 ===(严格比较),不会出现前面提到的弱类型比较坑;二是 match 是表达式,有返回值,不需要每个分支都手写 break,也不会有忘记写 break 导致 fall-through 的经典错误。唯一要注意的是如果没有命中任何分支、又没写 default,会直接抛 UnhandledMatchError,跟 switch 默认啥都不干的行为不一样,迁移老代码的时候这点容易漏。
4. 构造函数属性提升,DTO 类瘦身明显
1 | // PHP 7.4 写法 |
项目里一大批 DTO / Value Object 类,之前光是属性声明加构造函数赋值就占了一半篇幅,用属性提升之后代码量直接砍掉不少。需要注意的是属性提升只在构造函数里生效,如果同一个属性还想在其他方法里重新赋值做校验,还是得老老实实写完整的属性声明。
5. readonly 属性,不是深度不可变
PHP 8.1 加的 readonly,用起来体验很好,但有一个容易理解错的地方:
1 | class Order { |
这一层保护是有效的,items 属性本身不能被重新赋值。但如果 items 存的不是标量数组而是对象数组,readonly 保护的只是这个属性”指向哪个对象”,不保护对象内部的状态:
1 | class Order { |
跟前面 C# 的 record 踩过的坑其实是同一个道理——readonly/record 保证的都是”顶层引用不能变”,不是”整个对象图不可变”,属性本身如果是可变对象,内部状态还是能被外部改掉。真要不可变,OrderItem 自己的属性也得声明成 readonly。
6. 小结
这几个新特性单独看都不复杂,组合起来用(readonly + 构造函数属性提升 + 命名参数)写不可变 DTO 的体验已经很接近 C# 的 record 或者 TypeScript 的 interface。唯一要提醒自己的是 readonly 别想当然地当成”深度不可变”,属性是对象类型的时候该做防御性拷贝还是得做。