引言
PHP 8 是语言史上最大的一次现代化跃迁:从 8.0 的构造器属性提升、命名参数、联合类型、match 表达式,到 8.1 的枚举(Enum)、只读属性、第一类可调用对象、never 返回类型,再到 8.2/8.3 的只读类、类型系统加强——这门 30 岁的语言正在以惊人的速度补上工程化短板。
但「知道特性存在」和「在生产里用好」之间隔着一条鸿沟:枚举只是花哨语法糖吗?只读类什么时候才值得用?match 和 switch 到底该选谁?从 PHP 7 老项目迁移时,哪些变更会无声地破坏线上?本文用可运行的实战片段逐个拆解 PHP 8.x 的核心特性,并给出一条稳妥的迁移路线。
关联阅读:https://plumephp.com/php-laravel-internals/ 展示了框架如何受益于构造器属性提升;https://plumephp.com/php-testing-practice/ 演示如何用强类型特性写出可测试的领域模型。
目录
- 1. 类型系统:联合类型、纯类型与 never
- 2. 命名参数:告别位置参数地狱
- 3. 构造器属性提升:声明即赋值
- 4. match 表达式 vs switch 语句
- 5. null 安全操作符与空合并链
- 6. 枚举(Enum):有状态的常量集
- 7. 只读属性与只读类:不可变数据模型
- 8. 第一类可调用对象、new 与其余小特性
- 9. 从 PHP 7 迁移:破坏性变更与逐步策略
- 延伸阅读
1. 类型系统:联合类型、纯类型与 never
1.1 联合类型(PHP 8.0)
参数或返回值可声明为多个类型之一:
function formatAmount(int|float $amount): string
{
return number_format($amount, 2);
}
1.2 混合类型 mixed 与显式声明纪律
mixed 类型表示「任意类型」,滥用会削弱类型检查:
// ❌ 类型逃避
function log(mixed $data): mixed { return $data; }
// ✅ 明确契约
function log(string|array $data): void { /* ... */ }
1.3 never 返回类型(8.1)
never 表示函数永不正常返回(抛异常或退出):
function throwIfInvalid(array $data): never
{
throw new InvalidArgumentException('数据非法');
}
1.4 纯交集类型与静态返回
function same(Countable&Traversable $c): static
{
return $c; // 返回子类型自身
}
2. 命名参数:告别位置参数地狱
2.1 基本用法
只关心部分参数时,可以只传名字:
function createUser(
string $name,
string $email,
string $role = 'member',
bool $active = true,
): array { /* ... */ }
createUser('李雷', 'lei@example.com', role: 'admin', active: false);
2.2 与解构结合
$params = ['name' => '韩梅梅', 'email' => 'mei@example.com'];
createUser(...$params); // 数组展开为命名参数
2.3 注意事项
- 命名参数不要求按声明顺序,但不可重复传同名参数。
- 与可变参数
...$args混用时,多余命名参数会进$args。
3. 构造器属性提升:声明即赋值
3.1 传统写法 vs 提升写法
// PHP 7 时代:7 行样板
class User
{
private string $name;
public function __construct(string $name, private string $email)
{
$this->name = $name;
}
}
// PHP 8:提升语法,一行完成声明+赋值
class User
{
public function __construct(
public readonly string $name,
public string $email,
) {}
}
3.2 提升的可见性修饰符
提升时可用 public/protected/private/readonly 组合,同时保留默认值:
class Config
{
public function __construct(
public string $key,
public string $value = '',
private ?string $secret = null,
) {}
}
3.3 提升后的可读性收益
领域对象(DTO)是最大受益者:大量值对象只需「声明 + 构造」两行。结合只读类(第 7 节)效果更佳。
4. match 表达式 vs switch 语句
4.1 match 的严格比较
match 做严格相等比较(===),且是表达式(有返回值),而 switch 是弱比较语句:
$status = match ($code) {
200 => 'OK',
404 => 'Not Found',
500 => 'Server Error',
default => 'Unknown',
};
// 相比 switch:无穿透(fall-through)、自动返回、严格类型
4.2 表达式分支可多值
$bucket = match (true) {
$age < 18 => '未成年',
$age >= 18 && $age < 60 => '成年',
default => '老年',
};
4.3 缺省分支的强制
match 若所有分支都不匹配且无 default,抛 UnhandledMatchError——这能让「忘了处理新枚举值」变成可捕获的 bug 而不是静默错误。
5. null 安全操作符与空合并链
5.1 空安全访问
// PHP 7:层层判空
$country = $user->address ? $user->address->country : null;
// PHP 8:?-> 空安全链
$country = $user->address?->country;
// 更深的链
$phone = $user->profile?->contact?->phone;
5.2 空合并赋值(PHP 7.4+)
$config['retries'] ??= 3; // 未定义/为 null 时赋默认
5.3 组合使用
$value = $repo->find($id)?->price ?? 0.0;
6. 枚举(Enum):有状态的常量集
6.1 纯枚举
enum PaymentStatus
{
case Pending;
case Paid;
case Refunded;
}
$status = PaymentStatus::Paid;
echo $status->name; // 'Paid'
6.2 有值枚举(Backed Enum)
enum OrderState: string
{
case Created = 'created';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
public function label(): string
{
return match ($this) {
self::Created => '已创建',
self::Shipped => '已发货',
self::Delivered => '已送达',
self::Cancelled => '已取消',
};
}
}
OrderState::from('shipped'); // 从字符串反解
OrderState::tryFrom('nope'); // 反解失败返回 null
6.3 枚举方法、常量与接口
枚举可以:
- 定义方法(如上面的
label()) - 实现接口(每个 case 可独立实现行为)
- 使用常量(作为共用属性)
interface HasColor
{
public function color(): string;
}
enum Priority: int implements HasColor
{
case Low = 1; public function color(): string { return 'green'; }
case High = 2; public function color(): string { return 'red'; }
}
6.4 枚举 vs 类常量
| 场景 | 用类常量 | 用枚举 |
|---|---|---|
| 仅有字符串/整数常量 | ✅ 简单 | 可选 |
| 需要关联行为(label/color) | ❌ 需额外 map | ✅ 内聚 |
| 需要在 match/switch 中穷尽 | 弱 | ✅ 强制 |
| 作为参数类型约束 | 弱(string) | ✅ 类型安全 |
领域建模中,凡是有固定取值集合且行为相关的状态,优先枚举。
7. 只读属性与只读类:不可变数据模型
7.1 只读属性(8.1)
class User
{
public function __construct(
public readonly int $id,
public readonly string $email,
) {}
}
// $user->email = 'x'; // 抛 Error:不能修改只读属性
7.2 只读类(8.2)
类整体声明 readonly,所有属性隐式只读,且只能有被提升的构造器属性:
readonly class OrderItem
{
public function __construct(
public string $sku,
public int $qty,
public float $price,
) {}
}
7.3 只读类带来的价值
- 领域值对象天然不可变,避免「改一半」的脏状态。
- 与
match、enum组合可写出高内聚、无副作用的核心模型。 - 深拷贝/克隆语义更简单。
7.4 限制与权衡
- 只读属性不能在构造后赋值,包括
__clone、__wakeup。 - 嵌套对象仍是可变引用,只读不自动深度不可变。
- 需要依赖注入的服务类(通常可变有状态)不应声明只读。
8. 第一类可调用对象、new 与其余小特性
8.1 第一类可调用对象(8.1)
$strlen = strlen(...);
echo $strlen('hello'); // 5
$upper = strtoupper(...);
array_map($upper, ['a', 'b']); // ['A','B']
8.2 new 在初始化器中的使用(8.1)
function config(
Logger $logger = new ConsoleLogger(), // 默认值里 new
): void { /* ... */ }
8.3 字符串到数组的展开(8.1+)
$letters = str_split('ab c');
// 可直接用作列表解构
[$a, $b] = $letters;
8.4 其他值得知道的 8.x 特性
| 版本 | 特性 |
|---|---|
| 8.0 | 命名参数、联合类型、match、构造器提升、str_contains、str_starts_with |
| 8.1 | 枚举、只读属性、never、第一类可调用、fibers(协程底层) |
| 8.2 | 只读类、DNF 类型(`(A&B) |
| 8.3 | 类常量显式类型、json_validate、#[\Override] 属性、只读属性深克隆增强 |
9. 从 PHP 7 迁移:破坏性变更与逐步策略
9.1 最常见的破坏性变更
| 变更 | 影响 |
|---|---|
| 弱比较语义调整 | 0 == 'foo' 等怪异行为改变 |
| 字符串数字比较 | '10' < '2' 结果变化 |
each()/create_function() 移除 | 老代码直接报错 |
assert() 语义变化 | 依赖断言的老测试受影响 |
类型错误默认抛 TypeError | 大量字符串入参函数受影响 |
9.2 逐步迁移四步走
# 1. 先升级到 PHP 7.4(最低),加 declare(strict_types=1) 逐步收窄
# 2. 用 php8 兼容检查工具扫描
composer require --dev php-parallel-lint/php-parallel-lint
vendor/bin/parallel-lint src tests
# 3. 启用 PHPStan/PHP-CS-Fixer 提高类型与风格下限
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse --level=6 src
# 4. 逐个模块切到 PHP 8,修完回归再切下一模块
9.3 新代码的强类型纪律
declare(strict_types=1); // 文件顶部强制严格类型
// 接口优先,配合枚举与只读类,写出「编译期可检查」的领域模型
interface PaymentProcessor
{
public function process(Order $order): PaymentResult;
}
9.4 迁移心态
不要一次性重写全部代码。先在「新写的模块」里用新特性,老代码跑在严格类型 + 静态分析下,再用枚举/只读类逐步重构核心领域模型。语言特性只有在纪律下才有价值。
延伸阅读
- https://plumephp.com/php-laravel-internals/ — 框架中的构造器提升与容器注入配合
- https://plumephp.com/php-testing-practice/ — 用强类型特性写出可测试的领域模型
- https://plumephp.com/php-performance-tuning/ — JIT/OPcache 对类型化代码的性能红利
- PHP 8 新特性官方文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。