PHP 8 现代特性实战:枚举、只读类、构造器属性提升与强类型体系

全面实战 PHP 8.x 现代语言特性:命名参数、联合类型、构造器属性提升、match 表达式、null 安全操作符、枚举、只读类与只读属性、第一类可调用对象,并给出从 PHP 7 迁移到 8 的逐步策略与兼容性清单。

引言

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

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 新特性官方文档

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「php」更多文章

  1. PHP 面向对象与设计模式:SOLID、常用模式与 Laravel 实践
  2. PHP 静态分析与代码质量:PHPStan、Psalm、Rector 与 CI 门禁
  3. PHP 部署运维实战:Nginx、PHP-FPM、Docker 与 CI/CD