前言
这个问题在团队里通常以两种方式出现:一种是新项目立项,负责人在文档里写「基于 PHP 8.0」,理由是「供应商的服务器就装了这个版本」;另一种是老项目要横向复制一套新系统,团队想沿用旧版本以降低风险。
判断这个问题其实不需要讨论语法糖好不好用。它首先是一个生命周期问题:一个 PHP 版本从发布到完全停止接收安全补丁,窗口期是有限的。选一个已经停止安全支持的版本做新项目,等于在立项当天就背上一笔技术债——不是「以后再说」,而是「从第一天起就没有安全补丁可打」。
本文先把这个生命周期摆到桌面上,再逐项对比 8.0 与 8.3 的语言能力差距与迁移成本,最后给出一个可以直接跑的兼容性自检脚本。文中所有版本事实以官方支持版本表为准,建议在决策前再核对一次最新状态。
一、先看生命周期:这是决定性的一项
PHP 每个版本的维护分两个阶段:先是一段时间的功能维护(修 bug、加特性),之后进入只修安全问题的安全维护阶段,最后彻底结束支持(EOL)。在这之后,即使爆出高危漏洞也不会有官方补丁。
| 版本 | 发布状态 | 支持状态 | 用于新项目 |
|---|---|---|---|
| PHP 8.0 | 早已发布 | 已结束支持(安全支持已于 2023 年底终止) | 不要用 |
| PHP 8.1 | 早已发布 | 安全支持已结束 | 不建议 |
| PHP 8.2 | 早已发布 | 安全维护期内 | 可用但已偏旧 |
| PHP 8.3 | 早已发布 | 安全维护期内,窗口较长 | 可用,本文的推荐下限 |
| PHP 8.4 | 稳定 | 支持期内 | 推荐 |
| PHP 8.5 | 当前最新稳定版 | 支持期内 | 新项目首选 |
结论很清晰:如果只能在 8.3 和 8.0 之间二选一,答案是 8.3,而且差距不是「好一点」,是「有补丁」与「没补丁」的区别。但这里还要多说一句:题目把选择范围限定在 8.3 和 8.0,而在真实的新项目里,你完全可以选择当前的最新稳定版 8.5。选 8.3 的正确理由只有一个——生产环境的操作系统仓库里目前只提供 8.3。除此之外,没有理由主动选择一个非当前最新版。
具体的日期以官方支持版本表为准,不要凭记忆背书;决策文档里应当直接引用官方页面并记下核对日期。
二、语言能力差距:从 8.0 到 8.5 多了什么
把 8.0 之后每个版本的关键能力列出来,能直观看到「用 8.0」意味着要放弃多少东西。下面这张表里的版本对应关系均以官方发布为准:
| 能力 | 引入版本 | 对实际编码的价值 |
|---|---|---|
命名参数、match、构造器属性提升、联合类型、空安全操作符?-> | PHP 8.0 | 8.0 本身就是一次大跃迁,但这是下限 |
enum枚举、readonly属性、never返回类型、交叉类型A&B、Fiber | PHP 8.1 | 领域模型、并发原语 |
readonly类、DNF 类型 `(A&B)\ | C、Random\Randomizer、true/false/null` 独立类型 | PHP 8.2 |
json_validate()、#[\Override]、类型化类常量 | PHP 8.3 | 参数校验、重构安全 |
Property Hooks、不对称可见性、array_find()/array_find_key()/array_any()/array_all() | PHP 8.4 | 属性访问控制、数组查找 |
| 管道操作符 `\ | >、clone with、array_first()/array_last()` | PHP 8.5 |
举两个和业务代码关系最近的例子。
枚举(enum,PHP 8.1)。在 8.0 上,订单状态只能靠常量加注释:
<?php // PHP 8.0 的写法:约束全靠代码评审 final class OrderStatus { public const PENDING = 'pending'; public const PAID = 'paid'; public const SHIPPED = 'shipped'; } function ship(string $status): void { if ($status !== OrderStatus::PAID) { // 写错字符串也发现不了 throw new RuntimeException('状态不允许发货'); } }同一个需求在 8.1 及以上可以这样写,类型层面就被约束住了:
<?php declare(strict_types=1); enum OrderStatus: string { case Pending = 'pending'; case Paid = 'paid'; case Shipped = 'shipped'; public function canShip(): bool { return $this === self::Paid; } } function ship(OrderStatus $status): void { if (!$status->canShip()) { throw new RuntimeException('状态不允许发货'); } }差别不在于少写了几行,而在于非法状态在类型系统里根本不成立:传字符串进去会在调用点直接报错,而不是在业务逻辑深处才发现。
json_validate()(PHP 8.3)。在 8.3 之前,想判断一段字符串是不是合法 JSON,只能先json_decode再把结果丢掉,等于白白构造一遍数据结构。8.3 之后有了只做校验、不构造结果的函数:
<?php declare(strict_types=1); // 最低 PHP 8.3:json_validate() 是 PHP 8.3 引入的 $payload = '{"id":1,"name":"示例"}'; if (!json_validate($payload)) { http_response_code(400); exit('请求体不是合法 JSON'); } $data = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);顺带说一个容易混淆的点:json_validate()是PHP 8.3引入的,不是 8.4;而array_find()家族是PHP 8.4引入的。这两个函数的版本经常被记混,写文档时建议逐个核对。
三、性能:只讲机制,不给倍数
关于「升级能快多少」,网上到处是各种百分比,但它们几乎都不可复现——不同业务、不同框架、不同硬件上的结果差别巨大。本文不给任何倍速结论,只讲机制,你可以自己在自己的业务上测。
| 机制 | 引入版本 | 影响面 |
|---|---|---|
| OPcache 预加载 | PHP 7.4 | 减少每个请求编译与加载类的开销 |
| JIT 即时编译 | PHP 8.0 | 主要作用于 CPU 密集的长时间计算 |
| 类型系统与引擎优化 | 各版本持续 | 对常规 Web 请求影响有限 |
要强调的是JIT 是 PHP 8.0 引入的,它并不是从 8.3 才开始有的能力,所以「8.3 比 8.0 快是因为 JIT」这种说法本身就是错的。JIT 的收益高度依赖代码形态:典型的 Web 请求大部分时间花在等待数据库和网络,CPU 占比不高,JIT 带来的变化往往不明显;而纯计算的批处理脚本才更容易看到差异。
想评估升级收益,正确做法是自己做一次 A/B 实测,并保证两次测量的变量只有 PHP 版本:
# 用同一份代码、同一份数据、同一台机器,各跑三次取中位数 php8.0 -d opcache.enable_cli=1 bench.php php8.3 -d opcache.enable_cli=1 bench.php把「响应时间」「内存峰值」「每秒请求数」三项都记下来,而且要在真实流量回放或压测工具下测,不要只看一个合成循环。任何没有测试条件、测试方法说明的数字,都不应当写进决策文档。
四、迁移成本:从 8.0 到 8.3 要改什么
如果项目已经在 8.0 上运行,升级到 8.3 的代价主要来自这几个弃用点:
| 版本 | 变更 | 典型症状 |
|---|---|---|
| 8.1 | 向非空的内部函数参数传null被弃用 | 大量Deprecated警告,尤其是strlen(null)、htmlspecialchars(null) |
| 8.2 | 动态属性被弃用 | 给未声明的属性赋值时出现弃用警告 |
| 8.2 | 字符串中的${var}插值被弃用 | 模板与 heredoc 里出现弃用警告 |
| 8.3 | 部分函数与配置项继续收紧 | 以官方升级说明为准 |
注意这些当前都是弃用(deprecated),不是致命错误,代码通常还能跑,但日志会被警告刷满,而且它们会在下一个大版本变成错误。升级时应当把「日志里是否还有弃用警告」当作验收标准之一。
判断代码库里还有多少弃用点,除了跑测试用例,还可以用静态扫描工具(例如 PHP_CodeSniffer 生态里的 PHPCompatibility 规则集)按目标版本扫一遍。同时,下面的自检脚本可以在运行时快速给出「当前环境支持哪些能力」的结论:
<?php declare(strict_types=1); // 最低 PHP 8.0,可同时用 8.0 与 8.3 解释器运行,输出对比一目了然 printf("当前运行时:PHP %s\n", PHP_VERSION); printf("PHP_VERSION_ID:%d\n\n", PHP_VERSION_ID); // 1. 语言能力可用性 $features = [ '8.0' => ['命名参数', 'match 表达式', '构造器属性提升', '空安全操作符 ?->', 'JIT'], '8.1' => ['enum 枚举', 'readonly 属性', 'never 返回类型', '交叉类型 A&B'], '8.2' => ['readonly 类', 'DNF 类型 (A&B)|C', 'Random\Randomizer'], '8.3' => ['json_validate()', '#[Override]', '类型化类常量'], '8.4' => ['Property Hooks', 'array_find() 家族', '不对称可见性'], '8.5' => ['管道操作符 |>', 'clone with', 'array_first() / array_last()'], ]; foreach ($features as $ver => $list) { printf( "PHP %s %s:%s\n", $ver, version_compare(PHP_VERSION, $ver . '.0', '>=') ? '可用' : '不可用', implode('、', $list) ); } // 2. 关键函数是否存在(比版本号更直接) printf("\n"); foreach (['json_validate', 'array_find', 'array_any', 'array_first', 'fdiv', 'str_contains'] as $fn) { printf("函数 %-14s 存在:%s\n", $fn, function_exists($fn) ? '是' : '否'); } // 3. 扫描代码库中的弃用写法:字符串里的 ${var} 插值(PHP 8.2 起弃用) $dir = $argv[1] ?? __DIR__; $hits = 0; $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS) ); foreach ($it as $file) { if (!$file->isFile() || $file->getExtension() !== 'php') { continue; } $tokens = token_get_all((string) file_get_contents($file->getPathname())); foreach ($tokens as $token) { if (is_array($token) && $token[0] === T_DOLLAR_OPEN_CURLY_BRACES) { printf("弃用写法:%s 第 %d 行\n", $file->getPathname(), $token[2]); $hits++; } } } printf("\n共发现 %d 处使用花括号的变量插值写法\n", $hits);把这份脚本分别用php8.0和php8.3各跑一次,输出差异就是这次升级能拿到的东西。
常见坑点
1. 用「服务器上装的就是 8.0」当决策依据
❌ 立项文档写「环境已有 PHP 8.0,为降低风险保持一致」 —— 把运维的现状当成了技术选型的理由,而现状本身恰恰是要改的那一项。 ✅ 先确认能否安装新版本(容器镜像、系统仓库、官方源都能解决),再谈版本选择;确实装不了,就把「升级路径」写进项目计划。
2. 把 JIT 当成 8.3 相对 8.0 的性能优势
❌ 在方案里写「升级到 8.3 可以利用 JIT 提升性能」 —— JIT 是PHP 8.0引入的,两边都有。 ✅ 讲性能就讲机制并给出可复现的测量方法,不要写没有来源的倍数。
3. 用合成基准代替真实压测
❌ 拿一段空循环的耗时对比得出「快 X 倍」的结论并写进决策文档。 ✅ 用真实接口做压测(同一台机器、同一份数据、同一份配置),并且同时看响应时间、内存峰值与吞吐量。
4. 混淆json_validate()与array_find()的版本
❌ 把json_validate()写成 PHP 8.4 的特性,或者把array_find()写成 8.3 的 —— 依赖记错版本,代码在目标环境直接报「未定义函数」。 ✅json_validate()是8.3,array_find()家族是8.4,array_first()/array_last()是8.5,逐个核对。
5. 忽略composer.json里的 php 约束
❌ 代码里用了 8.1 的enum,但composer.json里写着"php": ">=8.0"——composer install不会报错,问题会一直潜伏到某台 8.0 的机器上。 ✅ 同步提升require.php的约束,并在 CI 里针对最低支持版本跑一遍测试。
6. 升级时只看「有没有报错」
❌ 脚本能跑通就算升级完成 —— 弃用警告被写进日志后无人问津,等到下一个大版本全变成致命错误。 ✅ 把「日志中不再出现新的弃用警告」列为验收项,并在 CI 里把 warning 级别打开。
7. 一次性跨越多个版本却不做中间验证
❌ 从 8.0 直接跳到 8.5,一次性改完所有代码再测 —— 出问题时无法判断是哪一档变更引起的。 ✅ 按 8.1 → 8.2 → 8.3 的节点分批升级,每跳一档都跑完整测试并观察日志。
8. 只升级了代码,没升级扩展与依赖
❌ 运行时换成 8.3,但某个依赖的 C 扩展还是为 8.0 编译的,加载时报 API 版本不匹配。 ✅ 升级前先列出所有扩展与 Composer 依赖,确认目标版本可用,再动手改代码。
总结
| 决策维度 | PHP 8.0 | PHP 8.3 |
|---|---|---|
| 安全支持 | 已结束 | 仍在维护期内 |
| 语言能力 | 缺少枚举、readonly、never等 | 具备 8.1~8.3 的全部能力 |
| 类型系统 | 联合类型、空安全操作符 | 加上枚举、交叉类型、DNF 类型 |
| 生态兼容 | 新版本框架与库陆续放弃 | 主流框架的新版本要求范围内 |
| 迁移成本 | —— | 主要来自 8.1/8.2 的弃用项 |
| 适合新项目 | 不适合 | 可用,是当前可接受的下限 |
如果只能在两者之间选,选 8.3,唯一成立的理由是生产环境的软件源暂时只提供它。但更应当说的是:新项目的版本选择不该被这两个数字框住——在 8.3 之上还有 8.4 与当前的稳定版 8.5,把「用哪个版本」变成一个需要论证的问题,而不是一个沿用旧习惯的默认值。