如何为旧项目逐步添加联合类型?

解读

国内存量 PHP 项目大多跑在 5.x/7.x 上,类型声明稀缺,逻辑耦合深,测试覆盖率低。面试官问“逐步添加联合类型”,不是让你背 string|int 语法,而是考察三件事:

  1. 能否在不中断业务的前提下做“灰度升级”;
  2. 是否熟悉 PHP8 联合类型的底层约束与运行时行为;
  3. 有没有工程化手段(静态分析、自动化测试、CI 卡口)保证升级后 7×24 高并发电商/金融/CRM 系统不出 P0 故障。
    回答必须给出“可落地的国内落地路线”,否则会被判定为“纸上谈兵”。

知识点

  1. 版本基线:PHP≥8.0 才支持联合类型,8.1 支持 readonly + 交集类型,8.2 支持 null/false 作为独立类型。
  2. 升级策略:
    • 先拉分支,锁定小版本(8.0.30),禁止开发新功能,只接受类型补丁;
    • 使用 rector/rectorUnionTypesRector 规则自动转换 @param string|int 注释为原生联合类型;
    • 通过 phpstan/psalm level=max 扫描,解决 mixed 隐式转换、协变/逆变违规;
    • 对核心支付、订单、库存接口加 #[\JetBrains\PhpStorm\Pure] 属性,防止副作用函数被误判为纯函数;
    • 灰度:按用户尾号 0-9 分批切流,配合阿里 SLS/腾讯云 CLS 实时日志,错误率>0.1% 自动回滚。
  3. 运行时兼容:
    • 联合类型在 OPcache 中会被编译为 IS_UNION 掩码,性能损耗 <1%,但需开启 opcache.validate_timestamps=0 防止频繁校验;
    • 老版本扩展(如 SeasLog、Xdebug<3.1)可能出现 ZEND_TYPE_IS_UNION 断言失败,需先升级扩展。
  4. 测试策略:
    • 单元测试用 PHPUnit 10covers-union 注解,确保每个分支类型都被覆盖;
    • 集成测试使用 php-fpmrequest_terminate_timeout=30s 压测,观察 502 数量;
    • 混沌工程:注入 grpc 超时、Redis 抖动,验证联合类型不会放大异常。
  5. 代码规范:
    • 遵循 PSR-12 的 declare(strict_types=1); 必须放在联合类型文件第一行;
    • 禁止在 __construct 中使用 string|mixed 这种“伪联合”,会被国内大厂代码评审直接打回。

答案

第一步:建立“类型升级基线”

  1. 在 GitLab 新建 php80-union 分支,.gitlab-ci.yml 里加两条卡口:
    • rector --dry-run --rule=UnionTypesRector 无 diff 才允许合并;
    • phpstan analyse --level=8 错误数为 0。
  2. 运维侧把灰度集群从 7.4.33 平滑升到 8.0.30,开启 opcache.preload=/opt/preload.php,预热 1w 文件,RT 上涨 <5% 才继续。

第二步:自动化批量改造

  1. 用 Rector 一次性扫描 app/{Service,Repository} 目录,把 2w 处 @param/@return 注释升级为联合类型;
  2. 对 500 个 array|ArrayObject 场景,手动收窄为 Collection|array<int,UserDto>,防止数组与对象混用导致 foreach 崩溃;
  3. 提交 Merge Request,CR 重点看:
    • 是否出现 string|resource(resource 不是合法联合类型,PHP8 会抛 Fatal);
    • 是否把 false 作为独立类型(PHP8.2 才支持,若仍在 8.0 需写成 bool)。

第三步:三层验证

  1. 静态层:psalm --taint-analysis 确保 SQL 注入、XSS 污点不会因联合类型被误判为 safe;
  2. 单元层:对 PriceService 里 calculate(int|float $amount): string@dataProvider 灌 0、-0、NAN、INF 四种边界值;
  3. 生产层:灰度 5% 流量 2 小时,错误日志关键字 TypeError|Union 为 0,CPU 利用率与 7.4 持平,则全量。

第四步:文档与回滚

  1. 在 Confluence 留“联合类型上线报告”,附 Grafana QPS、RT、502 三张截图,供审计;
  2. 若异常,30 秒内回滚到 7.4 镜像,PHP 8 的联合类型代码在 7.4 会直接语法错误,因此回滚后需自动 git revert 对应 commit,防止服务二次启动失败。

拓展思考

  1. 向下兼容场景:如果公司还有 ToB 私有化部署,客户环境锁 7.2,无法上联合类型,可写兼容层:
    # CompatibleTrait.php
    if (PHP_VERSION_ID >= 80000) {
        eval('function price($v): int|float {}');
    } else {
        function price($v) { return $v; }
    }
    
    通过 composer.jsonautoload.files 按需加载,保证同一份源码在 7.2/8.0 双轨运行。
  2. 与 JIT 叠加:PHP8 的 JIT 对联合类型有额外 guard 指令,高并发下可能放大 CPU cache miss;可在 php.ini 设置 opcache.jit=1205(function/trace 混合模式)+ opcache.jit_buffer_size=128M,再用 perf 观察 zend_jit 符号占比,>8% 就下调级别。
  3. 未来升级路径:联合类型 → 交集类型(PHP8.1)→ DNF 类型(PHP8.3),建议把“类型升级”做成常态流水线:每次小版本发布前,用 Rector 的 LevelSetList::UP_TO_PHPXY 自动升语法规整,配合代码评审,实现“永不过时”的祖传项目。