如何为旧项目逐步添加联合类型?
解读
国内存量 PHP 项目大多跑在 5.x/7.x 上,类型声明稀缺,逻辑耦合深,测试覆盖率低。面试官问“逐步添加联合类型”,不是让你背 string|int 语法,而是考察三件事:
- 能否在不中断业务的前提下做“灰度升级”;
- 是否熟悉 PHP8 联合类型的底层约束与运行时行为;
- 有没有工程化手段(静态分析、自动化测试、CI 卡口)保证升级后 7×24 高并发电商/金融/CRM 系统不出 P0 故障。
回答必须给出“可落地的国内落地路线”,否则会被判定为“纸上谈兵”。
知识点
- 版本基线:PHP≥8.0 才支持联合类型,8.1 支持
readonly+ 交集类型,8.2 支持null/false作为独立类型。 - 升级策略:
- 先拉分支,锁定小版本(8.0.30),禁止开发新功能,只接受类型补丁;
- 使用
rector/rector的UnionTypesRector规则自动转换@param string|int注释为原生联合类型; - 通过
phpstan/psalmlevel=max 扫描,解决mixed隐式转换、协变/逆变违规; - 对核心支付、订单、库存接口加
#[\JetBrains\PhpStorm\Pure]属性,防止副作用函数被误判为纯函数; - 灰度:按用户尾号 0-9 分批切流,配合阿里 SLS/腾讯云 CLS 实时日志,错误率>0.1% 自动回滚。
- 运行时兼容:
- 联合类型在 OPcache 中会被编译为
IS_UNION掩码,性能损耗 <1%,但需开启opcache.validate_timestamps=0防止频繁校验; - 老版本扩展(如 SeasLog、Xdebug<3.1)可能出现
ZEND_TYPE_IS_UNION断言失败,需先升级扩展。
- 联合类型在 OPcache 中会被编译为
- 测试策略:
- 单元测试用
PHPUnit 10的covers-union注解,确保每个分支类型都被覆盖; - 集成测试使用
php-fpm的request_terminate_timeout=30s压测,观察 502 数量; - 混沌工程:注入
grpc超时、Redis 抖动,验证联合类型不会放大异常。
- 单元测试用
- 代码规范:
- 遵循 PSR-12 的
declare(strict_types=1);必须放在联合类型文件第一行; - 禁止在
__construct中使用string|mixed这种“伪联合”,会被国内大厂代码评审直接打回。
- 遵循 PSR-12 的
答案
第一步:建立“类型升级基线”
- 在 GitLab 新建
php80-union分支,.gitlab-ci.yml里加两条卡口:rector --dry-run --rule=UnionTypesRector无 diff 才允许合并;phpstan analyse --level=8错误数为 0。
- 运维侧把灰度集群从 7.4.33 平滑升到 8.0.30,开启
opcache.preload=/opt/preload.php,预热 1w 文件,RT 上涨 <5% 才继续。
第二步:自动化批量改造
- 用 Rector 一次性扫描
app/{Service,Repository}目录,把 2w 处@param/@return注释升级为联合类型; - 对 500 个
array|ArrayObject场景,手动收窄为Collection|array<int,UserDto>,防止数组与对象混用导致 foreach 崩溃; - 提交 Merge Request,CR 重点看:
- 是否出现
string|resource(resource 不是合法联合类型,PHP8 会抛 Fatal); - 是否把
false作为独立类型(PHP8.2 才支持,若仍在 8.0 需写成bool)。
- 是否出现
第三步:三层验证
- 静态层:
psalm --taint-analysis确保 SQL 注入、XSS 污点不会因联合类型被误判为 safe; - 单元层:对 PriceService 里
calculate(int|float $amount): string用@dataProvider灌 0、-0、NAN、INF 四种边界值; - 生产层:灰度 5% 流量 2 小时,错误日志关键字
TypeError|Union为 0,CPU 利用率与 7.4 持平,则全量。
第四步:文档与回滚
- 在 Confluence 留“联合类型上线报告”,附 Grafana QPS、RT、502 三张截图,供审计;
- 若异常,30 秒内回滚到 7.4 镜像,PHP 8 的联合类型代码在 7.4 会直接语法错误,因此回滚后需自动
git revert对应 commit,防止服务二次启动失败。
拓展思考
- 向下兼容场景:如果公司还有 ToB 私有化部署,客户环境锁 7.2,无法上联合类型,可写兼容层:
通过# CompatibleTrait.php if (PHP_VERSION_ID >= 80000) { eval('function price($v): int|float {}'); } else { function price($v) { return $v; } }composer.json的autoload.files按需加载,保证同一份源码在 7.2/8.0 双轨运行。 - 与 JIT 叠加:PHP8 的 JIT 对联合类型有额外 guard 指令,高并发下可能放大 CPU cache miss;可在
php.ini设置opcache.jit=1205(function/trace 混合模式)+opcache.jit_buffer_size=128M,再用perf观察zend_jit符号占比,>8% 就下调级别。 - 未来升级路径:联合类型 → 交集类型(PHP8.1)→ DNF 类型(PHP8.3),建议把“类型升级”做成常态流水线:每次小版本发布前,用 Rector 的
LevelSetList::UP_TO_PHPXY自动升语法规整,配合代码评审,实现“永不过时”的祖传项目。