# PHP 8.0 新特性

Source: https://codewiki.com/zh/php/php8-features/

> - **what**: PHP 8.0 把更多契约写进语言本身：类型可以表达多个合法分支，实参可以按名称传递，元数据可以成为可反射的特性，分支与可空调用也有了专门语法。
> - **trap**: 新语法不会自动验证外部数据；命名实参还会让参数名成为兼容性表面，而 `match` 与 nullsafe 链的严格语义常被旧代码经验掩盖。
> - **fix**: 在边界先验证原始值，逐项测试联合类型和 `match` 分支，把公开参数名当作 API，并用反射测试确认特性的目标和构造参数。

## 是什么，为什么存在

PHP 8.0 新特性不是一套必须全部采用的风格规则，而是一组更精确的语言能力。本主题聚焦 PHP 8.0 引入、且持续影响日常应用代码的六项能力：联合类型、命名实参、特性、`match`、构造器属性提升和 nullsafe 运算符。PHP 8.1 的枚举与 Fiber 分别由相关主题讲解。

联合类型（union type）把多个允许的运行时类型写成 `int|string`。它能让引擎在函数调用、返回和属性赋值处执行已有类型规则，也能让静态分析器看见分支，但不会描述数组内部结构或自动解析请求字符串。

命名实参（named argument）使用 `parameter: value` 把值交给指定形参。它适合跳过中间的可选参数，也能让布尔值等含义不明显的实参更易读；代价是调用方开始依赖参数名，而不只是参数顺序。

特性（attribute）是附到类、方法、属性、参数等程序实体上的结构化元数据。框架或应用通过 Reflection API 读取它，再决定路由、校验或序列化行为。特性本身只描述信息，不会因为写在方法上就自动执行逻辑。

`match`、属性提升与 nullsafe 运算符解决三类常见样板代码：从输入到单一结果的严格分支、构造器参数到属性的重复声明，以及可空对象链上的逐层检查。它们压缩的是表达方式，不是业务决策；省掉的每一段代码仍有具体语义需要测试。

这些能力最常出现在 DTO、控制器、配置对象、领域值转换和框架集成中。迁移旧项目时，先用测试固定当前行为，再逐处采用新语法。大范围机械重写会同时改变类型错误、参数绑定、比较和空值传播，难以判断哪一项造成回归。

## 工作原理

六项能力作用在不同阶段。类型声明参与调用或赋值，命名实参在调用时绑定形参，特性由编译结果保存并等待消费者读取，`match` 和 nullsafe 运算符在表达式求值时工作，属性提升则在对象构造时建立并赋值属性。

| 能力 | 主要时机 | 语言保证 | 不提供的保证 |
| --- | --- | --- | --- |
| 联合类型 | 调用、返回、属性赋值 | 值符合至少一个成员类型 | 外部输入已通过领域校验 |
| 命名实参 | 调用绑定 | 按形参名选择目标 | 参数名可以随意重命名 |
| 特性 | 编译后保存、反射时读取 | 元数据可由 Reflection API 发现 | 元数据自动产生业务行为 |
| `match` | 表达式求值 | 严格比较、单分支、返回值 | 编译期穷尽性证明 |
| 属性提升 | 构造对象 | 声明属性并从形参赋值 | 自动校验领域不变量 |
| nullsafe 运算符 | 表达式求值 | 遇到 `null` 时短路并返回 `null` | 把缺失值变成业务默认值 |

### 类型与调用边界

联合类型的成员以 `|` 分隔。一个值只需符合其中一个成员；重复类型、`void` 与其他成员的组合等无意义声明会被拒绝。可空的单一类型通常写作 `?Customer`，复杂声明可以显式包含 `null`，但应选择一种团队风格并保持一致。

`declare(strict_types=1);` 会影响当前文件发起的用户自定义函数调用如何处理标量参数。它不会让 `int|string` 变成领域枚举，也不会检查 `array` 的键和值。来自 JSON、表单或数据库的值仍需在边界验证和转换，然后再进入带类型的内部接口。

命名实参先按名称找到形参，所以调用顺序可以不同，也可以跳过有默认值的形参。位置实参必须出现在命名实参之前；未知名称、同一形参重复赋值都会产生 `Error`。这不是关联数组配置语法，而是真正的函数调用绑定。

函数或方法一旦允许外部调用者使用命名实参，形参名就可能成为公开契约。把 `$includeTax` 重命名为 `$withTax` 不改变位置调用，却会让 `includeTax: true` 立即失败。库作者应把参数名纳入兼容性审查，应用内部也应避免仅为文案偏好频繁改名。

### 元数据与对象构造

特性使用 `#[Name(arguments)]` 语法，参数必须能作为常量表达式求值。一个特性类自身用内置的 `#[Attribute]` 标记，并可声明允许的目标和是否可重复。`ReflectionClass`、`ReflectionMethod` 等对象的 `getAttributes()` 返回 `ReflectionAttribute` 描述符。

取得描述符不等于构造特性对象。只有调用 `newInstance()` 才会运行特性类构造器，并在此时暴露错误参数、非法目标或未标记为特性类等问题。消费元数据的代码应决定何时实例化，也应为错误配置提供测试和清楚的启动失败。

构造器属性提升把可见性修饰符写在构造器形参前，例如 `public string $id`。该形参同时声明属性；构造时，传入值会在构造器主体执行前赋给属性，所以主体可以用 `$this->id` 校验组合不变量。没有可见性修饰符的形参仍只是普通形参。

提升属性不是不同种类的属性。反射、继承和类型规则仍按普通属性处理，也可以把特性直接附在提升形参上。不要同时再显式声明同名属性；重复声明会造成错误，而不是覆盖或补充提升声明。

### 严格分支与空值传播

`match` 表达式（match expression）把一个主题值与各分支条件按 `===` 比较。第一个匹配分支的右侧表达式成为整个 `match` 的值；没有贯穿执行，也不需要 `break`。多个条件可用逗号共享一个结果。

没有分支匹配且没有 `default` 时，PHP 会在执行到该路径时抛出 `UnhandledMatchError`。这让遗漏不能静默变成 `null`，但它不是编译期穷尽性检查。为封闭输入列出所有分支并测试每个值，通常比宽泛的 `default` 更能暴露需求变化。

`match (true)` 可以表达按顺序判断的条件区间，因为每个分支条件也按严格比较与 `true` 匹配。顺序因此成为业务规则的一部分：更窄或优先级更高的条件必须放在前面。若只是在映射离散值，直接匹配主题值通常更清楚。

nullsafe 运算符（nullsafe operator）写作 `?->`。左侧为对象时，它像 `->` 一样继续读取属性或调用方法；左侧为 `null` 时，剩余链短路，整个表达式得到 `null`，连当前方法的实参表达式也不会求值。

每个可能返回 `null` 的步骤都要使用 `?->`。链中出现普通 `->` 后，后一步再遇到 `null` 仍会报错。nullsafe 运算符只用于读取，不能放在赋值左侧，也不能取得引用；需要更新时，应先显式检查对象是否存在。

## 示例

下面四个示例以订单处理为同一背景，从类型与分支开始，再加入对象建模、特性元数据和 nullsafe 短路。所有输出都由本地 PHP 8.3.33 CLI 实际执行对应文件得到。

### 联合类型、命名实参与 `match`

状态转换函数接受内部整数码或外部字符串引用。`match` 的严格比较让整数 `1` 与字符串 `'1'` 保持不同含义，命名实参则说明第二个布尔值控制优先级。

<!-- quick -->

```php
// file: status_label.php
<?php
declare(strict_types=1);

function statusLabel(int|string $status, bool $priority = false): string
{
    return match ($status) {
        1 => 'queued',
        '1' => 'external-reference',
        2 => $priority ? 'priority' : 'processing',
        default => throw new InvalidArgumentException("unknown status: {$status}"),
    };
}

echo statusLabel(1), PHP_EOL;
echo statusLabel('1'), PHP_EOL;
echo statusLabel(status: 2, priority: true), PHP_EOL;

try {
    echo statusLabel(status: 9), PHP_EOL;
} catch (InvalidArgumentException $error) {
    echo $error::class, ': ', $error->getMessage(), PHP_EOL;
}
```

```text
queued
external-reference
priority
InvalidArgumentException: unknown status: 9
```

<!-- /quick -->

联合类型说明两种技术表示都合法，但函数仍明确拒绝未知状态。若使用 `switch` 或先把输入统一转换成字符串，整数码与外部引用可能被意外合并。测试应覆盖每个类型成员，以及看起来相等但类型不同的边界值。

`priority: true` 让调用含义比裸布尔值更直接。不过，调用现在依赖 `$priority` 这个名称；重命名形参需要同步修改所有命名调用，或作为公开 API 的破坏性变更处理。

### 用属性提升建立对象

订单与客户对象用提升属性去掉重复声明。构造器主体仍负责总额必须为正这一领域不变量；属性提升本身只完成声明和赋值。

```php
// file: promoted_order.php
<?php
declare(strict_types=1);

final class Address
{
    public function __construct(public string $city) {}
}

final class Customer
{
    public function __construct(
        public string $name,
        public ?Address $address = null,
    ) {}
}

final class Order
{
    public function __construct(
        public string $id,
        public Customer $customer,
        public int|float $total,
    ) {
        if ($this->total <= 0) {
            throw new InvalidArgumentException('total must be positive');
        }
    }

    public function summary(): string
    {
        $city = $this->customer->address?->city ?? 'pickup';
        return sprintf('%s|%s|%.2f', $this->id, $city, $this->total);
    }
}

$delivery = new Order(
    id: 'A-17',
    customer: new Customer('Mina', new Address('Paris')),
    total: 42.5,
);
$pickup = new Order(id: 'A-18', customer: new Customer('Noah'), total: 19);

echo $delivery->summary(), PHP_EOL;
echo $pickup->summary(), PHP_EOL;
```

```text
A-17|Paris|42.50
A-18|pickup|19.00
```

命名实参允许第二次构造在一行中仍保持可读。`total` 的联合类型保留整数和浮点数两种输入，但 `sprintf()` 在输出边界将它们规范为同一种金额格式。真实金额通常还需要明确的精度和货币策略，不能由联合类型替代。

`address` 是可空属性，因此访问 `city` 的那一步使用 `?->`。`?? 'pickup'` 是业务默认策略，与 nullsafe 运算符的传播语义分开；如果缺少地址应阻止下单，就应在构造器或服务边界拒绝，而不是显示默认值。

### 定义并读取可重复特性

`RequiresRole` 只允许用于方法，而且可以重复。读取器先筛选目标特性，再调用 `newInstance()` 得到带类型的配置对象。

```php
// file: route_attributes.php
<?php
declare(strict_types=1);

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class RequiresRole
{
    public function __construct(public string $role) {}
}

final class OrderController
{
    #[RequiresRole('support')]
    #[RequiresRole('admin')]
    public function refund(): void {}
}

$method = new ReflectionMethod(OrderController::class, 'refund');
$attributes = $method->getAttributes(RequiresRole::class);

foreach ($attributes as $attribute) {
    $rule = $attribute->newInstance();
    echo $method->getName(), ':', $rule->role, PHP_EOL;
}
```

```text
refund:support
refund:admin
```

程序能输出角色，是因为消费代码显式读取、实例化并解释特性。若没有这一段 Reflection 逻辑，`#[RequiresRole]` 不会执行授权。生产授权还必须从可信身份取得角色，并定义多个特性是「满足任意一个」还是「必须全部满足」。

`IS_REPEATABLE` 与 `TARGET_METHOD` 描述结构约束，不描述上面的组合政策。新增特性消费者时，应测试无特性、单个特性、重复特性、错误参数和错误目标，而不只测试成功路径。

### 观察 nullsafe 短路

最后一个示例让方法实参带有可见副作用。资料不存在时，`auditLabel()` 不会求值；对象链存在时，它才会在 `format()` 调用之前运行。

```php
// file: nullsafe_profile.php
<?php
declare(strict_types=1);

final class Profile
{
    public function __construct(public ?DeliveryAddress $address = null) {}
}

final class DeliveryAddress
{
    public function __construct(public string $city) {}

    public function format(string $audit): string
    {
        return "{$audit}:{$this->city}";
    }
}

function auditLabel(): string
{
    echo "audit evaluated\n";
    return 'ship';
}

function destination(?Profile $profile): string
{
    return $profile?->address?->format(auditLabel()) ?? 'pickup';
}

echo destination(new Profile()), PHP_EOL;
echo destination(new Profile(new DeliveryAddress('Paris'))), PHP_EOL;
```

```text
pickup
audit evaluated
ship:Paris
```

短路可以避免无用工作，但把有必要的审计、计数或校验放进方法实参会造成条件性执行。副作用如果无论对象是否存在都必须发生，应在 nullsafe 链之前单独执行，并用清楚的控制流处理缺失对象。

这里两个可空步骤都使用 `?->`：`$profile` 可能为空，`address` 也可能为空。只在第一个位置使用 nullsafe，然后写 `->address->format()`，无法保护第二个空值。

## 陷阱

### 把联合类型当作输入校验

> **陷阱:** `int|string` 只约束运行时类型，不会判断字符串是不是允许的状态、整数是否在范围内，也不会检查数组元素。宽泛联合类型还可能把一个本应统一的数据模型扩散到整个代码库。

**修复方法：** 在 HTTP、CLI、数据库或消息边界先验证原始形状和值，再转换成窄小的内部表示。测试联合类型的每个成员、边界值和被拒绝的类型；当分支拥有不同业务行为时，考虑值对象或枚举，而不是继续增加成员。

### 忘记命名实参绑定的是参数名

> **陷阱:** 生成代码常把所有调用改成命名形式，却仍把形参改名当作无风险重构。旧名称会在运行时成为 `Unknown named parameter`，而测试若只使用位置调用就发现不了。

**修复方法：** 对公开方法记录稳定参数名，并在集成测试中保留至少一个命名调用。只在跳过可选参数或显著提升可读性时使用命名实参；重命名公开形参时按兼容性变更迁移调用方。

### 认为特性会自动执行策略

> **陷阱:** `#[RequiresRole('admin')]` 只是元数据。没有消费者时，它不会检查身份；消费者若只调用 `getAttributes()` 而从不实例化，也可能把错误构造参数和非法目标留到很晚才暴露。

**修复方法：** 明确哪个启动或请求组件读取特性、何时调用 `newInstance()`、怎样处理重复项。用反射集成测试遍历已注册处理器，使错误配置在部署或启动阶段失败；安全策略还要测试缺少元数据时的默认行为。

### 用 `default` 隐藏新增分支

> **陷阱:** 宽泛的 `default` 会让未来输入或枚举 case 自动沿用旧政策。相反，完全省略 `default` 又只会在遗漏路径实际执行时抛出 `UnhandledMatchError`，并不提供编译期证明。

**修复方法：** 对封闭集合显式列出分支，并让数据提供器遍历所有合法值。只有所有未来值确实共享一个安全后备策略时才使用 `default`；外部未知输入应在进入 `match` 前得到验证和拒绝。

### 让 nullsafe 吞掉领域缺失

> **陷阱:** `?->` 把技术上的可空访问变成 `null`，但不判断缺失资料是正常、可重试还是数据损坏。链尾随手添加 `?? ''` 会进一步抹掉「没有对象」与「字段为空」的区别。

**修复方法：** 先定义每个 `null` 的领域含义，再决定传播、默认或抛出异常。测试链中每个可空位置，而不只测试第一个；必须执行的日志、授权和校验不要藏在可能被短路的方法实参中。

### 迁移时顺带改变行为

> **陷阱:** 把构造器改成提升属性、把 `switch` 改成 `match`、再加入联合类型的单次提交会同时改变赋值、比较和失败模式。旧 `switch` 可能依赖宽松比较或贯穿执行，直接替换后即使语法正确也会改变结果。

**修复方法：** 每次只改变一种语义，并在修改前用特征测试记录旧输入与输出。先处理 PHP 8.0 的不兼容变更和依赖，再采用新语法；对每个 `switch` 检查比较类型、`break`、贯穿路径和默认分支。

<!-- deep -->

## 特性交互与兼容性

PHP 8.0 的语法常在同一个构造器或控制器中组合出现。组合并不会取消各自的边界：提升属性仍受类型规则约束，命名实参仍依赖形参名，附在提升形参上的特性仍需消费者解释，nullsafe 链产生的 `null` 仍需业务代码决定如何处理。

### 参数绑定的顺序

调用发生时，位置实参先按顺序占用形参，命名实参再按名称选择尚未赋值的形参。把位置实参放到命名实参之后是语法错误；两次指定同一形参，或使用不存在的名称，则产生 `Error`。参数默认值只在该形参没有被任何实参绑定时使用。

数组展开会保留字符串键，并把它们当作命名实参处理。这使 `function(...$payload)` 看起来方便，却把不可信数组直接变成调用协议：未知键、重复绑定与类型错误都可能在调用处出现。外部载荷应先通过允许列表投影到已知字段，而不是原样展开。

可变参数会收集没有匹配到已声明形参的命名实参，并在数组中保留字符串键。这是有意支持扩展选项时可用的能力，但也可能掩盖拼写错误。公开 API 应明确是否接受任意命名选项，并在需要封闭集合时自行拒绝未知键。

### 特性的惰性实例化

Reflection API 可以按特性类名筛选描述符，而不先运行特性构造器。这让扫描器能低成本收集元数据，也意味着只扫描不实例化的测试不足以验证配置。需要特性对象的消费者应在受控边界调用 `newInstance()`，并把失败报告到具体类、方法和特性位置。

特性参数保存的是编译期可表示的值，而不是任意运行时代码。对象真正创建时，构造器仍可验证字符串格式、数值范围和参数组合。目标掩码限制「可以附在哪里」，构造器限制「参数是否有意义」，消费器则负责「这些元数据怎样影响行为」。

重复特性的合并规则不属于 PHP 语言。授权可以解释为任一角色满足即可，校验规则可能要求全部通过，路由特性也可能注册多条路径。特性类使用 `IS_REPEATABLE` 只允许重复出现，应用仍必须写明顺序、组合和冲突政策。

### `match` 的失败时间

`match` 对主题和分支条件使用严格比较，因此从旧 `switch` 迁移时，字符串数字、布尔值和 `null` 都要重新检查。右侧只允许一个表达式；需要多步工作时，应调用一个命名函数，而不是把副作用压进复杂表达式。

分支条件按顺序求值，并只执行选中的结果表达式。使用 `match (true)` 时，重排条件可能改变结果；范围边界相交时尤其如此。测试应覆盖每个分界点的前一个值、边界值和后一个值。

`UnhandledMatchError` 属于 `Error` 分支，可由 `Throwable` 边界捕获，但不应在局部转换成无信息的默认结果。它通常表示代码没有处理一个实际到达的状态。记录主题值和调用上下文后，让请求或任务边界按既定失败政策结束，通常比继续执行更安全。

### nullsafe 链的求值

nullsafe 链从左到右求值。某一步得到 `null` 后，该链剩余的属性访问、方法调用和对应实参都不会发生。括号中的独立表达式可能形成自己的求值范围，因此不要只凭视觉位置猜测副作用是否执行。

`?->` 的结果不能作为赋值目标，例如 `$customer?->address = $address` 不合法。更新需要明确的所有者和失败策略：先检查 `$customer`，缺失时抛出、创建对象或返回失败，再执行普通赋值。显式分支也让静态分析和审查更容易看到状态变化。

长链会隐藏哪个关系允许为空。超过两三个步骤时，把中间结果赋给有业务名称的变量，或在模型上提供返回明确结果的方法。这样可以分别记录「客户不存在」「地址缺失」和「城市缺失」，而不是全部压成同一个 `null`。

## PHP 8.0 的其他能力

PHP 8.0 还引入了多项适合按需采用的能力。下面只列出可准确界定的语义，不给出没有基准环境的性能结论。

| 能力 | 核心语义 | 使用边界 |
| --- | --- | --- |
| `mixed` 类型 | 显式表示可接受任意值，也包含 `null` | 优先放在真正开放的边界，不要替代可表达的窄类型 |
| `static` 返回类型 | 返回类型随运行时调用类变化 | 适合返回 `$this` 的继承式流畅接口 |
| `throw` 表达式 | 可在箭头函数、`??`、三元和 `match` 中抛出 | 复杂失败处理仍应使用清楚的语句块 |
| `str_contains()` 等 | 提供包含、前缀和后缀判断 | 仍需决定大小写、编码和空字符串政策 |
| `WeakMap` | 对象键不阻止该对象被回收 | 适合附加元数据，不替代拥有明确生命周期的缓存 |
| JIT | OPcache 可把部分执行路径编译为机器码 | 收益取决于工作负载，必须在自己的部署环境测量 |

`mixed` 是信息最少的类型，不是「兼容性最好」的默认选择。内部函数若只处理 `string|array`，写出联合类型能让调用者、引擎和工具共同维护这个边界。只有容器、序列化入口或通用转发层确实接收任意值时，`mixed` 才准确。

`str_contains($haystack, '')`、`str_starts_with($haystack, '')` 和 `str_ends_with($haystack, '')` 都会返回 `true`。搜索词来自用户时，空字符串是否表示「匹配全部」必须由产品规则决定。新函数消除了 `strpos()` 与 `false` 混淆，却不会替你定义领域含义。

`WeakMap` 的键必须是对象。当外部没有其他强引用时，条目可随键对象回收，因此不能把它当作持久缓存或唯一数据存储。它适合给对象附加派生信息，同时不因为缓存本身延长对象生命周期。

JIT 是 PHP 8.0 的重要运行时能力，但 Web 应用常受数据库、网络和序列化等其他成本限制。没有固定硬件、配置、数据和重复次数，就不能声称某段应用会获得具体倍数。先用代表性请求和 CPU 密集任务分别建立基线，再决定复杂度是否值得。

## 迁移验证

采用新特性之前，应先确认运行时、扩展、Composer 依赖和部署镜像都满足目标版本。只在开发机运行 PHP 8 不足以证明队列工作进程、计划任务与生产 FPM 使用相同二进制和配置。

第一轮测试应保持旧代码，专门发现 PHP 8.0 不兼容变化。重点包括内部函数参数错误现在更常抛出 `TypeError`、字符串与数字的非严格比较规则变化，以及错误抑制和资源到对象的迁移。具体清单应以官方迁移指南和项目实际 API 为准。

第二轮再逐项引入语法。每次把一个 `switch` 改成 `match`，都比较原主题值类型和所有贯穿路径；每次加入联合类型，都运行来自真实边界的样本；每次引入命名实参，都搜索公开形参的调用方与继承覆盖。

静态分析能发现一部分类型、不可达分支和调用问题，但不能替代运行时反射与集成测试。特性消费者、动态调用、框架容器和序列化边界依赖运行时数据。迁移完成的证据应同时包含静态检查、单元测试和代表性入口的执行结果。

把最低支持版本写入 `composer.json`，让 CI 在该版本上执行测试，同时可在当前目标版本上增加一个任务。最低版本暴露误用新语法，目标版本暴露弃用与未来兼容性问题。两者通过后，才能把本地语法成功当作可部署证据。

<!-- /deep -->

[检查点: php/php8-features](https://codewiki.com/zh/php/php8-features/#checkpoint)

## 延伸阅读

- [PHP 8.0 新特性](https://www.php.net/manual/en/migration80.new-features.php)
- [PHP 类型声明](https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.union)
- [PHP 特性概述](https://www.php.net/manual/en/language.attributes.overview.php)
- [PHP 命名实参](https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments)
- [PHP `match`](https://www.php.net/manual/en/control-structures.match.php)
- [PHP nullsafe 运算符](https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.nullsafe)
