紧接上一篇 PHP 7.4 vs 8.0 全面对比,本篇聚焦 8.0 → 8.1 的升级路线。和 7→8 那种"范式级跨越"不同,8.0→8.1 是一次增量型升级:破坏性变更极少,新特性以"补齐 8.0 未覆盖的语法糖"为主,但枚举(Enums)和 Fiber(协程)两个重量级特性的引入,让 8.1 在工程化能力上了一个台阶。

PHP 8.1 发布于 2021 年 11 月,Bugfix 支持到 2022 年 6 月,安全修复到 2024 年 12 月。写本文时(2026 年 10 月)8.1 也已经 EOL 快两年了。但和 8.0 一样,8.1 仍然是很多"老项目想升到新范式但又不想一步跨到 8.2"的中间停靠站——尤其是 8.1 引入的枚举和 readonly 属性,很多框架(Laravel 9+ / Symfony 6+ / ThinkPHP 8+)都已经在核心代码里使用。

一、核心差异速查表

维度 PHP 8.0 PHP 8.1 影响程度
枚举 (Enums) ❌ 无,需用常量/自定义 class 模拟 ✅ 原生支持,带方法、类型、cases() 代码层重磅
readonly 属性 ❌ 无 ✅ 类属性声明后只读,构造器赋值后不可变 可选改进
Fiber 协程 ❌ 无(只有 ext-swoole/amp 等 PECL 扩展) ✅ 内置 Fiber 类,可挂起/恢复执行 性能/架构
never 返回类型 ❌ 只有 void ✅ 原生 never 表示"永不返回" 可选改进
第一类可调用语法 ['Foo', 'bar'] / Foo::class . '::bar' Foo::bar(...) 直接获取 Closure 语法糖
数组辅助函数 array_key_first/last(7.3 引入) 新增 array_is_list()、array_diff_assoc_key() 可选改进
Serializable 接口 ✅ 正常工作 ⚠️ Deprecated,8.2 起改用 __serialize/__unserialize 破坏性
JIT 基础支持,缓冲区固定 改进:支持函数级别 JIT 配置,profile 模式优化 性能
对象复制 clone 全量拷贝 readonly 对象 clone 后属性不可变(构造器只读一次) 语义变更
支持状态(2026-10) ❌ 完全 EOL(2023-11) ❌ 完全 EOL(2024-12) 安全风险
💡 8.0→8.1 升级的一个好消息:PHP 官方给 8.0→8.1 的升级定性是"minor version upgrade,大部分项目应该可以不做任何修改就能运行"(引自 Migration 8.1 Breaking Changes 页面)。实际经验表明,90% 的业务代码直接升到 8.1 不需要改任何东西——和 7→8 那种"改到怀疑人生"的体验完全不同。

二、PHP 8.1 三大重磅新特性

1. 枚举(Enums)—— 8.1 最值得升级的理由

在枚举出现之前,PHP 开发者表示"一组有限的常量值"有三种方式:define 常量、class 常量(const)、或者自己写一个带静态属性的类。三种都有缺陷——常量没有类型、无法 switch、不能带方法。8.1 原生枚举把这个问题一次性解决了:

// PHP 8.0:用常量模拟枚举
class OrderStatus {
    const PENDING = 'pending';
    const PAID = 'paid';
    const SHIPPED = 'shipped';
    const CANCELLED = 'cancelled';
}

// 问题:没有类型系统,任何字符串都能传进来
public function updateStatus(string $newStatus) {}  // 谁能保证传的一定是合法值?

// PHP 8.1:原生枚举
enum OrderStatus: string {
    case PENDING = 'pending';
    case PAID = 'paid';
    case SHIPPED = 'shipped';
    case CANCELLED = 'cancelled';

    // 枚举可以带方法
    public function isFinal(): bool {
        return match($this) {
            self::SHIPPED, self::CANCELLED => true,
            default => false,
        };
    }

    // 可以带静态工厂
    public static function fromLabel(string $label): self {
        foreach (self::cases() as $case) {
            if ($case->label() === $label) return $case;
        }
        throw new InvalidArgumentException("Unknown label: $label");
    }
}

// 使用:类型安全、IDE 自动补全、switch 可穷举检查
public function updateStatus(OrderStatus $status): void {
    if ($status->isFinal()) {
        throw new LogicException('Cannot change a final status');
    }
    $this->status = $status;  // 存的是 enum 对象,不是字符串
}

$status = OrderStatus::PAID;
echo $status->value;  // "paid"(string 类型枚举)
echo $status->name;   // "PAID"
OrderStatus::cases();  // [OrderStatus::PENDING, ...]
OrderStatus::from('paid');  // 反向查找(backed enum 可用)

枚举分两种:纯枚举(Pure Enum)不带值、Backed 枚举绑定 string/int。Backed 枚举自动提供 from() / tryFrom() 静态方法做反向查找(从存储值还原枚举对象),这在数据库反序列化、API 参数校验场景下特别好用。

Laravel 9+ 的 Model 里 castAsEnum() 方法就是为 8.1 枚举准备的,Symfony 的 EnumType 表单组件也在 6.1 加了原生支持。如果你现在还在项目里用常量模拟枚举,8.1 是彻底告别这种反模式的好时机。

2. readonly 属性 —— 不可变对象的第一步

8.1 引入的 readonly 关键字,让类属性在构造器赋值后就不能再修改,实现了"构造时一次性写入"的语义:

class ProductVO {
    public readonly string $name;
    public readonly float $price;
    public readonly string $sku;

    public function __construct(string $name, float $price, string $sku) {
        $this->name = $name;
        $this->price = $price;
        $this->sku = $sku;
    }
}

$p = new ProductVO('T-shirt', 99.9, 'SKU-001');
$p->name = 'Polo';  // Error: Cannot modify readonly property ProductVO::$name

readonly 和 DDD(领域驱动设计)里的 Value Object 概念完美契合:一个 VO 创建后就应该保持不可变,任何"修改"操作都应该返回一个新实例而不是原地修改。readonly 不是"语法糖"——它是 PHP 语言层面强制不可变语义,能帮你把很多隐式 bug(比如意外修改了作为数组 key 的对象、或者异步任务里多个协程同时修改一个状态对象)消灭在编译期。

配合 8.1 的构造器属性提升,VO 的写法可以再简化一层:

readonly class ProductVO {
    // readonly 类:所有属性自动 readonly
    public function __construct(
        public string $name,
        public float $price,
        public string $sku,
    ) {}
}
// ↑ 这在 8.2 才正式支持 readonly class 语法(8.1 只能逐个属性写 readonly)
// 8.1 的写法:
class ProductVO {
    public readonly string $name;
    public readonly float $price;
    public readonly string $sku;
    public function __construct(string $name, float $price, string $sku) {
        $this->name = $name;
        $this->price = $price;
        $this->sku = $sku;
    }
}

3. Fiber —— PHP 内置协程时代的开端

Fiber 是 PHP 8.1 被讨论最多的新特性,也是最容易被误解的特性。先说 Fiber 是什么:它是一个语言级的、可以被挂起和恢复的执行上下文,让 PHP 代码在 不修改现有语法 的情况下实现协程调度。

和 swoole、amphp 等"需要换 runtime、需要用 yield 或 async/await"的协程方案不同,Fiber 让你可以写这样的代码:

// PHP 8.1 Fiber 基本用法
$fiber = new Fiber(function (int $a, int $b): int {
    $sum = $a + $b;
    Fiber::suspend($sum);  // 挂起并返回中间值
    return $sum * 2;
});

$result1 = $fiber->start(3, 4);     // 返回 7(suspend 的值)
$result2 = $fiber->resume(10);       // 返回 14(最终值)

但 Fiber 本身只是基础原语,它不提供事件循环、不提供 HTTP 客户端、不提供数据库驱动——这些需要由上层库(比如 amphp 或 reactphp 的 Fiber 分支)来实现。换句话说,8.1 引入 Fiber 是把"能不能做协程"这个问题从"换 swoole runtime"变成了"不换 runtime 也能做",但离"开箱即用的异步框架"还有几步。

所以现实建议是:不要为了 Fiber 单独升 8.1。等 8.2/8.3 时代,当主流框架(Laravel/Symfony)的 Fiber 生态成熟了,再一起升不迟。但 8.1 引入 Fiber 这个事实本身很重要——它意味着 PHP 官方终于承认了"并发不是扩展的事,是语言的事"。

三、8.0→8.1 破坏性变更清单

好消息:8.0→8.1 的破坏性变更非常有限,大部分是 Deprecated 警告级别的。下面列的是升级过程中最有可能撞到的几个。

1. 父类方法签名变化(Contravariance 进一步收紧)

// PHP 8.0:子类参数类型可以更严格(Contravariance 还没完全实施)
class Base {
    public function find($id) {}  // 无类型
}
class Child extends Base {
    public function find(int $id) {}  // 8.0 警告,8.1 变成 Fatal Error
}

// 正确做法:子类参数类型必须"不窄于"父类
// 即父类参数是 A,子类可以用 A 或 A 的父类,但不能用 A 的子类
class Child extends Base {
    public function find(int|string $id) {}  // ✅
    public function find(mixed $id) {}      // ✅ 最宽松
}

2. Serializable 接口 Deprecated

// PHP 8.0:可以这样写
class MyClass implements Serializable {
    public function serialize() { return serialize($this->data); }
    public function unserialize($data) { $this->data = unserialize($data); }
}

// PHP 8.1:触发 Deprecated
// PHP 8.2:正式移除 Serializable 接口,必须改用 Magic Methods
class MyClass {
    public function __serialize(): array { return ['data' => $this->data]; }
    public function __unserialize(array $data): void { $this->data = $data['data']; }
}

8.1 只是给 Deprecated 警告,所以你有足够时间去改。Rector 有现成的规则可以一键把所有 Serializable 实现转换成新写法。

3. 内置函数参数默认值变化

少数函数的默认行为在 8.1 有调整,最可能撞到的两个:

  • strftime() 的 locale 处理在 Windows 上有变化(底层用了不同的 C runtime);
  • imagecreatefromstring() 对非法图片数据的处理,8.0 返回 false,8.1 在某些场景下会触发 ValueError。

这类变更频率很低,建议用 Rector 的 php81 规则集自动扫一遍。

四、升级实操指南

8.0→8.1 的升级流程比 7→8 简单得多,大多数项目只要改 composer.json 再 update 一次就能跑起来。下面按场景分条列出。

路径 A:Composer 项目升级(90% 的情况,5 分钟搞定)

# 1. 修改 composer.json
# 把 "php": "^8.0.0" 改成 "^8.1.0"
# 同时更新 config.platform.php
# "config": { "platform": { "php": "8.1.31" } }

# 2. 升级
composer update --with-all-dependencies
composer install --no-dev  # 如果是生产环境

# 3. 跑 Rector(可选,自动扫 Deprecated)
composer require rector/rector --dev
vendor/bin/rector process src --set php81

# 4. 清缓存 + 冒烟测试
php artisan optimize:clear   # Laravel
php bin/console cache:clear  # Symfony
# 然后访问首页、跑一遍自动化测试

Composer 项目升级 8.1 最容易踩的坑不是 PHP 本身的兼容性,而是依赖包的 composer.json 里写死了 "php": "~8.0" 这种版本约束。遇到这种情况:

# 方法 1:--ignore-platform-reqs 强制升级(最后手段)
composer update --ignore-platform-reqs

# 方法 2:升级那个依赖包到支持 8.1 的最新版本
composer update problematic/package --with-all-dependencies

# 方法 3:换包(如果原作者不再维护)
# composer require alternative/package
# composer remove problematic/package

路径 B:Windows 官方包升级

# 1. 下载 PHP 8.1 官方包
#    https://windows.php.net/download/
#    选 VS16 x64 Non Thread Safe / Thread Safe(与你 8.0 包类型一致)

# 2. 备份旧 php.ini,解压新包到新目录

# 3. 修改 PATH 把 PHP 8.1 提到最前

# 4. 迁移扩展:把旧目录 ext/ 下自定义扩展(swoole 等)复制过去
#    注意扩展版本也要对应 8.1(旧版本的 .dll 可能不兼容)

# 5. 验证
php -v   # 应该显示 PHP 8.1.x
php -m   # 确认必要扩展都在

路径 C:源码编译升级

和 8.0 编译的 configure 参数几乎完全一样,只需要改版本号:

cd /usr/local/src
wget https://www.php.net/distributions/php-8.1.31.tar.xz
tar xvf php-8.1.31.tar.xz
cd php-8.1.31

# configure 参数基本与 8.0 相同
./configure \
    --prefix=/usr/local/php81 \
    --enable-fpm \
    --enable-opcache \
    --enable-jit \
    --with-jit  \
    --enable-mysqlnd \
    --with-pdo-mysql \
    --enable-mbstring \
    --with-curl \
    --with-zip \
    --with-intl \
    --with-openssl \
    --enable-bcmath

make -j$(nproc)
sudo make install

# 切换默认 PHP(用 update-alternatives 或直接改 PATH)
# systemctl restart php81-fpm  # 如果是 FPM 模式

五、8.0→8.1 与 7.4→8.0 的难度对比

对比维度 7.4 → 8.0 8.0 → 8.1
Composer constraint 变动 几乎所有包都要调 少数包写死 ~8.0 的需要调
代码改动量(估算) 5%~20%(动态属性、内建函数签名、字符串偏移等) 0%~3%(几乎不需要改)
Rector 覆盖度 高(大部分破坏点可自动修复) 极高(Deprecated 扫描为主,修复点很少)
升级后可能的线上事故 函数调用签名、ReturnTypeError、动态属性 Error 几乎没有
升级时间(中小项目) 半天 ~ 2 天 10 分钟 ~ 2 小时
值不值 必须升(7.4 已 EOL) 看需求(想用到枚举/readonly/Fiber 再升)

六、要不要升 8.1?还是直接跳 8.2?

在 2026 年这个节点上,直接升 8.2 或 8.3 可能比停留在 8.1 更划算。原因:

  • 8.1 自身也已经 EOL 两年了,再升的话下一次升级目标就是 8.3 或 8.4,8.1→8.2 的过渡反而多一层;
  • Laravel 11、Symfony 7、ThinkPHP 8 等主流框架的最新版都明确要求 PHP 8.2+,停在 8.1 会错过框架更新;
  • 8.2/8.3 相比 8.1 的增量(readonly class、first-class callables 正式化、property hooks 雏形),对工程化的提升比 8.0→8.1 那次更显著。

推荐路线:如果代码量不大、框架可升,直接从 8.0 一步跳到 8.2/8.3。如果有历史包袱要拆、或者想先"温和"过渡到新范式,那就 8.0→8.1→8.2 拆两步走——但第二步不要等太久。

参考链接