一、升级PHP8后,为啥突然冒出来一堆致命错误?

1.1 以前的PHP对类型有多“宽容”

很多老开发者可能都有印象,PHP7及更早版本里,几乎不会因为类型不匹配报致命错误。举个最简单的例子,你写一个加法函数,只随便写两个参数:

<?php
// 技术栈:PHP 7.x
function add($a, $b) {
    return $a + $b;
}
// 调用的时候传字符串,比如add("5",3),结果会自动转成整数8,根本不会报错
echo add("5", 3); // 输出8,PHP7根本不会说你传错类型

这时候PHP就像一个不挑快递的外卖员,不管你给他送的是数字、字符串甚至数组,他都能试着帮你处理,哪怕处理结果不对,也只是给个警告,不会直接打断程序运行。但这种“宽容”也带来了很多隐性问题:比如你本来想处理数字,结果不小心传了字符串,程序跑了很久才出奇怪的结果,找bug找半天。

1.2 PHP8的联合类型,到底改了啥规矩?

到了PHP8,PHP团队推出了联合类型这个新特性,简单说就是:你可以给函数的参数或者返回值,指定它能接受几种类型中的任意一种,比如“这个参数只能是整数或者字符串”“返回值要么是字符串要么是布尔值”。而且这一次,PHP的检查规矩变严了——不是像以前那样给个警告,而是只要传的类型不符合,直接报致命错误,程序直接终止

还是用刚才的加法函数举例,升级到PHP8后,如果你给它加上联合类型声明:

<?php
// 技术栈:PHP 8.x
function add(int|string $a, int|string $b): int|string {
    return $a + $b;
}
// 这时候如果你传数组进去,比如add([1,2], 3),PHP8会直接报致命错误,说参数类型不对
add([1,2], 3); // 触发致命错误:Argument #1 ($a) must be of type int|string, array given

是不是突然就报错了?这就是很多老项目升级PHP8后,第一时间遇到的问题——以前“默认能跑”的代码,现在因为加了联合类型(或者PHP8自动启用了更严的类型检查),直接挂了。

二、最常见的联合类型错误场景拆解

2.1 参数类型不匹配:传错类型就炸锅

这类错误是最多的,比如旧项目里很多函数没有明确的类型声明,或者只声明了单个类型,升级后改成联合类型,调用的地方不小心传了其他类型的数据。举个电商项目里的例子:

<?php
// 技术栈:PHP 8.x
// 旧代码里用户ID可能传数字或者字符串,现在明确成联合类型
function getOrder(int|string $orderId): array {
    // 从数据库查订单,返回数组
    return ['order_id' => $orderId, 'status' => 'paid'];
}
// 旧代码里不小心传了布尔值(比如某个地方取到了true,误以为是订单ID)
getOrder(true); // 触发致命错误:Argument #1 ($orderId) must be of type int|string, bool given

很多时候,这类错误的源头是旧项目里的变量来源复杂,比如从接口取的数据、从表单来的数据可能是布尔值,或者从缓存里取的null,以前PHP7会自动转,现在PHP8的联合类型不允许,直接报错。

2.2 返回值类型不匹配:该返回的类型没返回

除了参数,返回值的联合类型检查也会引发错误。比如旧代码里的函数,本来应该返回字符串或者布尔值,但实际返回了整数或者其他类型,现在PHP8会检查到:

<?php
// 技术栈:PHP 8.x
function checkOrderStatus(int $orderId): string|bool {
    if ($orderId < 1) {
        return false; // 订单ID无效,返回布尔值,符合联合类型要求
    }
    // 模拟查订单,比如订单号是123,返回字符串状态
    return "订单已支付";
}
// 但是如果不小心写错,比如在某个分支返回了整数:
function wrongCheckOrder(int $orderId): string|bool {
    if ($orderId < 1) {
        return false;
    } else if ($orderId == 999) {
        return 0; // 这里返回了整数,不是string或bool,报错!
    }
    return "订单已支付";
}

这类错误很隐蔽,旧项目里可能藏在某个很少用到的分支里,平时跑测试没覆盖到,升级PHP8后突然被触发,导致线上服务报错。

三、联合类型的应用场景和优缺点

3.1 哪些场景适合用联合类型?

联合类型不是随便用的,它适合的场景是:函数的参数或返回值确实有明确的几种合法类型,没有歧义的情况。比如刚才的用户ID,要么是数据库里的数字ID,要么是前端传过来的字符串ID(比如"usr_123"),这时候用int|string就非常明确,比以前用mixed(任意类型)好太多。还有像返回值可能是成功的字符串,或者失败的布尔值,用联合类型能让调用者一眼就知道这个函数的返回值可能有几种情况,IDE也能更好的做代码提示,比如你调用checkOrderStatus的时候,IDE会告诉你返回值可以是string或bool,不用猜。

3.2 联合类型的优缺点

先说好的地方:第一,代码可读性变强,别人看你的函数声明,立刻就知道能传啥、能返回啥,不用猜;第二,减少隐性bug,以前PHP7里传错类型可能跑很久才出问题,现在升级后直接报错,线上更容易发现问题;第三,IDE支持更好,写代码的时候IDE会提前提示类型错误,不用等跑起来才发现。

不好的地方也很明显:第一,老项目升级的时候,会瞬间冒出大量错误,很多老开发者不适应,以为PHP变“坏”了;第二,容易滥用,比如明明参数只有一种类型,非要加联合类型,比如函数只接受int,非要写int|string,反而会误导别人;第三,初期适配成本高,要改很多旧代码,特别是大型项目,改起来头疼。

3.3 用联合类型要注意啥?

第一,别滥用,能用单个类型就不用联合类型,比如参数只能是int,就写int,别搞int|string,除非真的有两种情况;第二,不确定的时候先不用,比如你还没搞清楚这个参数可能传啥,就先用mixed,等搞清楚了再改联合类型;第三,一定要配合严格模式,PHP8里的declare(strict_types=1);这个配置一定要开,能避免自动类型转换的隐性问题;第四,别忽略null,PHP8里联合类型可以加null,比如int|null,代表参数可以是int或null,这时候你要小心,null传进去会不会出问题,一定要处理null的情况。

四、平滑迁移的实用策略

4.1 先试点,别全量改

很多人升级PHP8后,第一反应是把所有函数都改成联合类型,结果一下子炸出几百个错误,根本改不完。正确的做法是:先挑几个核心模块,比如用户模块、订单模块,先改这部分的函数,测一个星期,没问题再扩到其他模块。比如先改用户相关的函数,测注册、登录、个人中心的功能,没问题再改商品模块,这样压力小很多。

4.2 用兼容层,临时过渡

如果旧代码里确实有很多不符合联合类型的地方,别急着直接改,先写一层兼容的代码,比如在函数开头先把参数转成合法类型,或者加错误处理。比如刚才的getOrder函数,旧代码里有时候会传数组过来,你可以先兼容:

<?php
// 技术栈:PHP 8.x
function safeGetOrder(int|string $orderId): array {
    // 兼容旧代码,传数组的话,把数组转成字符串ID
    if (is_array($orderId)) {
        $orderId = implode('_', $orderId);
    }
    // 兼容传null的情况,比如null转成0
    if ($orderId === null) {
        $orderId = 0;
    }
    // 再判断类型,确保合法
    if (!is_int($orderId) && !is_string($orderId)) {
        throw new InvalidArgumentException("订单ID类型错误,仅支持int或string");
    }
    return ['order_id' => $orderId, 'status' => 'paid'];
}

这样改的话,旧代码可以继续跑,同时新代码可以用这个联合类型,慢慢替换原来的函数,等旧代码里传错类型的情况都改完了,再把兼容层去掉。

4.3 用工具提前扫描错误

升级PHP8前,最好先跑一下PHP静态分析工具,比如PHPStan或者Psalm,这些工具能提前扫描代码里的类型不匹配问题,不用等升级后才发现。比如用PHPStan,配置好后,跑一下就会列出所有类型错误的地方,你可以先把这些错误改完,再升级PHP8,这样升级后遇到的错误就会少很多,特别是大型项目,这个工具非常好用。

4.4 临时关闭严格类型,过渡后再开

如果实在是改不动了,短时间内要升级PHP8,可以先在php.ini里设置error_reporting,或者在每个文件开头加declare(strict_types=0);,这样PHP会允许自动类型转换,比如把字符串"5"转成int,把数字5转成字符串,这样能暂时避免很多致命错误,等以后慢慢改。不过这个只是临时方案,长期还是要开strict_types=1,因为严格类型能减少很多隐性bug。

五、总结

升级到PHP8后的联合类型引发的错误,本质上是PHP从“弱类型、宽容检查”向“强类型、严格检查”的转变,这是好事,虽然初期会痛苦,但长期来看,能让代码更安全、更清晰,减少很多因为类型问题引发的线上bug。只要做好迁移策略,先试点、用兼容层、借工具,就能平滑过渡,享受到PHP8带来的新特性。记住,联合类型不是麻烦,是帮你写更靠谱代码的工具,别害怕它,只要按步骤来,就能顺利升级。