PHP 8.1在2021年11月25日正式发布了,这是PHP 8系列的第二个次要版本,带来了很多令人期待的新特性:枚举(Enums)、Fiber(纤程)、只读属性(Readonly Properties)、交叉类型(Intersection Types)、never返回类型、数组解包支持字符串键名、first-class可调用语法等。
我们团队一直关注PHP的新版本,PHP 8.1发布后,我们决定把一个中等规模的项目从PHP 7.4升级到PHP 8.1,顺便用新特性重构一些代码。本来以为升级很简单,结果踩了很多坑,熬了好几个夜才搞定。
今天这篇文章,分享升级过程中遇到的问题和解决方案,以及新特性的使用心得。希望能帮到打算升级PHP 8.1的朋友,少踩坑少熬夜。
一、为什么升级到PHP 8.1
先说说为什么要升级。我们的项目之前一直用PHP 7.4,PHP 7.4在2022年11月就会停止安全支持,继续用有安全风险。而且PHP 8.0和8.1带来了很多新特性和性能提升,升级后代码可以更简洁,性能也更好。
PHP 8.1的主要新特性:
- 枚举(Enums):终于有原生枚举了,不用再用类常量或者第三方库
- Fiber(纤程):原生的协程支持,可以做异步编程
- 只读属性(Readonly Properties):属性只能赋值一次,不可变对象更方便
- 交叉类型(Intersection Types):参数必须同时满足多个类型约束
- never返回类型:函数永远不会返回(抛异常或exit)
- 数组解包支持字符串键名:之前只能解包数字键名的数组
- first-class可调用语法:可以用
$fn = strlen(...)的方式获取可调用对象 - 性能提升:PHP 8.1比7.4快大概10%到15%
这些特性都很吸引人,尤其是枚举和只读属性,能让代码质量提升不少。于是我们决定升级。
二、升级前的准备
升级前我们做了一些准备工作。
第一,检查扩展兼容性。PHP 8.1是新发布的版本,很多扩展还没来得及适配。我们项目用了大概二十多个扩展,逐个检查是否支持PHP 8.1。大部分主流扩展(PDO、redis、memcached、gd、mbstring等)都已经支持了,但有几个冷门扩展还没支持,我们要么等更新,要么找替代方案。
第二,检查代码兼容性。用PHP_CodeSniffer加PHPCompatibility规则,扫描代码中可能不兼容PHP 8.1的写法。比如被废弃的语法、被移除的函数等。扫描出来几十个问题,大部分是小问题,改起来不难。
第三,搭建测试环境。在测试服务器上装PHP 8.1,部署项目,跑单元测试和集成测试。这一步发现了很多问题,下面详细说。
三、踩坑一:废弃的特性导致报错
PHP 8.1废弃了一些特性,用了就会报Deprecated warning。如果项目里把warning当错误处理(很多框架默认这样),就会直接报错。
我们遇到的主要有这些:
1. 传null给非空参数
PHP 8.1开始,给非空类型的参数传null会报Deprecated。比如:
function foo(string $bar) {}
foo(null); // Deprecated: Passing null to parameter #1 ($bar) of type string is deprecated我们项目里有很多地方从数据库或者接口取数据,可能是null,然后直接传给了非空参数的函数。在PHP 7.4里这是允许的(会有warning但不明显),PHP 8.1里就报Deprecated了。
解决方案:要么给参数加?string可空类型,要么在传参前做null检查和默认值处理。我们花了不少时间改这些,大概改了一百多处。
2. 自动转换的精度丢失
PHP 8.1对浮点数转整数的处理更严格了。比如(int) "12.5"之前会得到12,现在会报Deprecated,因为字符串转整数有精度丢失。
解决方案:用intval()或者先转float再转int,明确处理。
3. 其他废弃特性
还有一些小的废弃,比如$a == $b其中一个是null另一个是字符串时的比较规则变了,htmlentities()默认编码变了等。这些都比较好改,看报错信息改就行。
四、踩坑二:枚举(Enum)的使用问题
枚举是PHP 8.1最让人期待的特性之一。我们项目里之前用类常量模拟枚举,升级后打算改成原生枚举。结果用的时候踩了一些坑。
坑一:枚举不能继承
PHP的枚举不能继承其他类,也不能被继承。我们之前有一个基础的枚举类,里面有一些通用方法,然后各个具体枚举继承它。改成原生枚举后,不能继承了,通用方法只能用trait或者在每个枚举里重复写。
解决方案:把通用方法放到trait里,枚举use这个trait。
trait EnumHelper {
public static function values(): array {
return array_column(self::cases(), 'value');
}
}
enum Status: string {
use EnumHelper;
case Active = 'active';
case Inactive = 'inactive';
}坑二:枚举值的类型
枚举分两种:纯枚举(Unit Enum)和 Backed Enum(有标量值的枚举)。纯枚举没有value属性,只有name。Backed Enum必须指定类型(int或string),每个case必须有唯一的值。
我们最开始写枚举的时候,有的加了类型有的没加,导致使用的时候混乱。后来统一规定:需要存数据库或者传值的用Backed Enum,纯状态标记用纯枚举。
坑三:枚举和数据库的交互
用枚举后,存数据库的时候要存枚举的value,取出来的时候要转成枚举对象。我们用的是Laravel框架,Laravel 8还不支持原生枚举的类型转换(Laravel 9才支持),所以需要自己写访问器和修改器。
public function getStatusAttribute($value): Status {
return Status::from($value);
}
public function setStatusAttribute(Status $status) {
$this->attributes['status'] = $status->value;
}这个有点麻烦,但也能接受。如果用的是支持枚举的ORM就方便很多。
坑四:枚举的序列化
枚举不能直接json_encode,会得到一个空对象。需要实现JsonSerializable接口,或者手动转成value。
enum Status: string implements JsonSerializable {
case Active = 'active';
case Inactive = 'inactive';
public function jsonSerialize(): string {
return $this->value;
}
}五、踩坑三:只读属性(Readonly)的问题
只读属性也是PHP 8.1的新特性,属性只能在构造函数里赋值一次,之后不能修改。这对不可变对象很有用。
但用的时候也有坑:
坑一:只读属性不能有默认值
只读属性不能在声明时给默认值,必须在构造函数里赋值。这是因为只读属性的设计是"构造时赋值,之后不可变",如果有默认值就违背了这个设计。
class Foo {
public readonly string $bar = 'default'; // Fatal error
}解决方案:在构造函数里给默认值。
class Foo {
public function __construct(
public readonly string $bar = 'default'
) {}
}坑二:只读属性不能在构造函数外赋值
这个是当然的,但我们遇到了一个问题:用工厂方法创建对象的时候,工厂方法里不能给只读属性赋值,必须通过构造函数。所以工厂方法只能调用构造函数,不能先new再赋值。
解决方案:工厂方法里调用构造函数传参,或者用命名参数。
坑三:克隆对象时只读属性也不能改
克隆对象的时候,__clone方法里也不能修改只读属性。如果需要克隆后修改某些属性,那些属性就不能设为readonly。
六、踩坑四:Fiber(纤程)的使用问题
Fiber是PHP 8.1引入的原生协程,能让函数暂停和恢复,是异步编程的基础。我们很兴奋地试了一下,结果发现坑不少。
坑一:Fiber不是完整的异步方案
Fiber只是提供了协程的基础能力,本身没有事件循环、IO调度、网络异步等功能。要做真正的异步编程,还需要基于Fiber的框架(比如Swoole、Revolt、AMP等)。单纯用Fiber,只能做一些手动的协程切换,实用性有限。
我们最开始以为用了Fiber就能像Go那样写异步代码,结果发现不是那么回事,还得配合事件循环。
坑二:Fiber的API比较底层
Fiber的API很底层,需要手动start、resume、suspend。写起来比较繁琐,也容易出错。比如:
$fiber = new Fiber(function(): void {
Fiber::suspend('first');
Fiber::suspend('second');
});
echo $fiber->start(); // first
echo $fiber->resume(); // second
echo $fiber->resume(); // null这种写法比Go的goroutine繁琐很多,也不如Python的async/await直观。
坑三:扩展兼容性
很多PHP扩展还不支持Fiber,在Fiber里调用某些扩展的函数可能会出问题。比如数据库驱动、缓存驱动等,需要确认是否支持Fiber。
我们试了一下,在Fiber里用PDO查询数据库,是阻塞的,并没有异步效果。因为PDO本身不支持异步。要真正异步,得用Swoole的协程客户端。
所以,Fiber目前更像是一个基础设施,为以后的异步生态打基础。普通项目暂时不用急着用,等基于Fiber的异步框架成熟了再用也不迟。
七、踩坑五:其他新特性的问题
交叉类型(Intersection Types)
交叉类型要求参数同时满足多个类型约束,比如function foo(A&B $obj),$obj必须同时实现A和B接口。这个特性用得不多,但在某些场景很有用。
坑是:交叉类型不能和联合类型一起用,也不能用在属性上(只能用在参数和返回值)。而且如果两个接口有同名方法,会有冲突。
never返回类型
never表示函数永远不会返回,要么抛异常,要么调用exit/die。这个类型很有用,能让静态分析更准确。
坑是:如果函数声明了never返回类型,但实际上有return语句,会报错。而且never类型的函数不能有返回值,连return;都不行。
数组解包支持字符串键名
PHP 7.4开始支持数组解包([...$a, ...$b]),但之前只支持数字键名的数组,字符串键名会报错。PHP 8.1开始支持字符串键名了。
坑是:如果两个数组有相同的字符串键名,后面的会覆盖前面的,和array_merge的行为一样。这个要注意,不要以为会像数字键名那样追加。
first-class可调用语法
可以用$fn = strlen(...)获取可调用对象,比Closure::fromCallable('strlen')简洁多了。
坑是:这个语法只能用于可调用的东西,不能用于普通函数表达式。而且有些IDE还不支持这个语法的自动补全。
八、性能提升和其他变化
升级到PHP 8.1后,性能确实有提升。我们做了基准测试,同样的代码,PHP 8.1比7.4快大概12%左右,内存占用也少了一些。对于我们这种Web应用,响应时间平均快了几十毫秒,虽然不是质的飞跃,但也不错。
PHP 8.1还有一些其他变化:
- MySQLi:默认错误模式改为异常。之前MySQLi默认是静默错误,PHP 8.1改成了抛异常。如果代码里没有处理异常,可能会报错。我们的项目用的是PDO,不受影响,但如果用MySQLi要注意。
- fileinfo扩展:finfo函数的默认选项变了。影响不大。
- GD库:一些函数的参数变化。如果用GD处理图片要注意。
- 各种废弃和移除。一些很老的函数和特性被移除了,比如MySQL扩展早就没了,PHP 8.1又移除了一些。
九、升级的建议和经验
最后总结一些升级的建议和经验。
第一,不要急着升。 PHP 8.1刚发布的时候,很多扩展和框架还没适配。建议等一两个月,等生态成熟了再升。我们是PHP 8.1发布后两周升的,遇到了一些扩展不兼容的问题,等了一周才解决。如果不急,可以等8.1.1或者8.1.2再升,更稳定。
第二,先在测试环境充分测试。 不要直接在生产环境升级。先在测试环境跑通所有功能,跑单元测试和集成测试,确认没问题了再上生产。我们在测试环境测了两周,改了很多问题,才敢上生产。
第三,逐步升级,不要一步到位。 如果项目很大,可以先升级PHP版本但不用新特性,确保代码能跑起来。然后再逐步用新特性重构代码。不要一边升版本一边大改代码,出了问题不好排查。
第四,关注废弃警告。 PHP 8.1的Deprecated warning很多,虽然不影响运行,但最好都改掉,因为未来版本可能会变成Fatal error。可以把Deprecated级别暂时调低,但要记录下来,慢慢改。
第五,新特性按需使用。 不要为了用新特性而用新特性。枚举适合状态管理,只读属性适合不可变对象,Fiber适合异步框架。用对了能提升代码质量,用错了反而增加复杂度。
第六,做好回滚方案。 万一升级后出问题,要能快速回滚到旧版本。我们的做法是生产环境先灰度,切10%流量到PHP 8.1,观察没问题再全量。有问题随时切回PHP 7.4。
十、写在最后
以上就是我们升级PHP 8.1的踩坑记录。从废弃特性到枚举、只读属性、Fiber,再到性能和升级建议,尽量写得全面。
总的来说,PHP 8.1是一个值得升级的版本。新特性很实用,性能有提升,代码可以写得更简洁更安全。但升级过程中确实会遇到一些坑,需要耐心解决。
PHP这几年发展很快,从PHP 7的性能革命,到PHP 8的JIT和类型系统,再到PHP 8.1的枚举和Fiber,PHP越来越现代化了。虽然很多人调侃PHP是"最好的语言",但不可否认,PHP在不断进步,依然是Web开发的重要选择。
2021年了,PHP 8.1已经发布,PHP 8.2也在开发中。希望PHP越来越好,也希望更多开发者能享受到新版本带来的好处。
如果你也在考虑升级PHP 8.1,希望这篇文章能帮你少踩坑。如果有其他问题或者经验,欢迎在评论区交流。
祝大家升级顺利,代码越写越优雅。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录