PHP 7.4即将在今年年底正式发布,虽然还没正式发布,但是beta版本已经出来了,新特性也基本确定了。我们团队最近对一个老项目做了PHP 7.4的迁移测试和准备工作,踩了不少坑,也积累了一些经验。
今天这篇文章,就来聊聊PHP 7.4的新特性,以及我们迁移过程中的实战经验,包括新特性介绍、兼容性问题、性能提升、迁移步骤、踩坑记录等,希望能给准备迁移到PHP 7.4的朋友一些参考。
一、为什么要迁移到PHP 7.4
先说说为什么要迁移到PHP 7.4。
首先,PHP 7.4带来了很多新特性,比如类型属性、箭头函数、预加载、FFI等,这些新特性能让代码更简洁、更安全、性能更好,也能提升开发效率。
其次,PHP 7.4的性能有提升,虽然不如PHP 7.0到PHP 7.1那么大,但是也有一定的提升,尤其是在WordPress等应用上,性能提升比较明显。对于我们这种流量比较大的项目,性能提升意味着能节省服务器成本,还是很有吸引力的。
第三,PHP的版本支持周期,每个版本有两年的全面支持,然后一年的安全支持。PHP 7.1已经在2019年12月停止支持了,PHP 7.2也会在2020年11月停止支持,所以为了安全和维护,我们需要升级到更新的版本。
最后,我们的项目用的是PHP 7.2,虽然还能用,但是一些新的框架和库,已经开始要求PHP 7.3甚至更高的版本了,为了能用上新的技术和工具,我们也需要升级PHP版本。
综合考虑,我们决定在PHP 7.4正式发布之后,就把项目迁移过去,所以提前做了迁移测试和准备工作,下面就来聊聊我们的经验。
二、PHP 7.4的主要新特性
先介绍一下PHP 7.4的主要新特性,这些是我们比较关注的,也是迁移之后能用上的。
1. 类型属性(Typed Properties)
这是PHP 7.4最重要的新特性之一,终于支持类属性的类型声明了。之前,PHP只支持方法参数和返回值的类型声明,不支持类属性的类型声明,我们只能用PHPDoc注释来声明属性类型,但是没有运行时检查。
PHP 7.4之后,我们可以直接给类属性声明类型,比如:
class User {
public int $id;
public string $name;
public ?string $email;
public array $roles = [];
}这样,PHP会在运行时检查属性的类型,如果赋值的类型不对,就会抛出TypeError,能更早地发现bug,让代码更安全。而且,类型属性还能提升性能,因为PHP知道了属性的类型,可以做一些优化。
类型属性支持所有的PHP类型,包括int、string、bool、float、array、object、callable、iterable,还有类和接口类型,也支持可空类型(?string),但是不支持void和callable作为属性类型,也不支持默认值的类型推断。
这个特性,对于我们这种大型项目来说,很有用,能让代码更规范,更安全,我们迁移之后,准备逐步给核心的类加上类型属性。
2. 箭头函数(Arrow Functions)
箭头函数也是PHP 7.4的一个重要新特性,让闭包的写法更简洁。之前,PHP的闭包写法比较繁琐,比如:
$factor = 10;
$nums = array_map(function($n) use ($factor) {
return $n * $factor;
}, [1, 2, 3]);有了箭头函数之后,可以写成:
$factor = 10;
$nums = array_map(fn($n) => $n * $factor, [1, 2, 3]);简洁很多,而且箭头函数会自动捕获外层的变量,不需要用use,很方便。
箭头函数的语法是fn(参数列表) => 表达式,只能有一个表达式,不能有多个语句,返回值就是这个表达式的结果。箭头函数也支持类型声明和返回类型,比如fn(int $x): int => $x * 2。
这个特性,对于经常用arraymap、arrayfilter等函数的人来说,很有用,能让代码更简洁,更易读。我们的项目里有很多这样的代码,迁移之后,准备逐步改成箭头函数的写法。
3. 有限返回类型协变(Limited Return Type Covariance)
PHP 7.4支持了有限的返回类型协变和参数类型逆变。之前,PHP的子类方法,返回类型必须和父类完全一样,参数类型也必须完全一样,不能更具体或者更宽泛。
PHP 7.4之后,子类的返回类型可以比父类更具体(协变),参数类型可以比父类更宽泛(逆变),比如:
class Animal {}
class Cat extends Animal {}
interface AnimalShelter {
public function adopt(string $name): Animal;
}
class CatShelter implements AnimalShelter {
public function adopt(string $name): Cat { // 返回类型更具体,协变
return new Cat();
}
}这样,代码更灵活,也更符合面向对象的设计原则。不过,这个特性是有限的,只支持返回类型协变和参数类型逆变,不支持属性类型的协变和逆变,而且只在自动加载的类中生效,因为需要完整的类信息。
这个特性,对于我们这种有复杂继承关系的项目来说,很有用,能让代码更灵活,更符合面向对象的设计。
4. 数组展开运算符(Array Spread Operator)
PHP 7.4支持在数组中使用展开运算符(...),之前只能在函数参数中使用。现在可以这样写:
$parts = ['apple', 'pear'];
$fruits = ['banana', 'orange', ...$parts, 'watermelon'];
// ['banana', 'orange', 'apple', 'pear', 'watermelon']这样,数组合并更简洁,也更高效,因为展开运算符是语言结构,比array_merge更快。而且,展开运算符还能用于Traversable对象,不只是数组。
这个特性,对于经常需要合并数组的人来说,很有用,能让代码更简洁,性能也更好。
5. 空合并赋值运算符(Null Coalescing Assignment Operator)
PHP 7.4新增了??=运算符,也就是空合并赋值运算符。之前,我们经常写这样的代码:
$this->request->data['key'] = $this->request->data['key'] ?? 'default';有了??=之后,可以写成:
$this->request->data['key'] ??= 'default';简洁很多,而且只计算一次左边的表达式,性能更好。这个运算符,对于经常需要给变量设置默认值的场景,很有用。
6. 预加载(Preloading)
预加载是PHP 7.4的一个重要性能特性,可以在PHP启动的时候,把一些常用的类和函数预加载到内存里,这样每次请求的时候,就不需要再加载和解析这些文件了,能大幅提升性能。
预加载的配置很简单,在php.ini里设置opcache.preload指向一个PHP脚本,这个脚本里用opcachecompilefile()或者require来加载需要预加载的文件。比如:
// preload.php
opcache_compile_file('/path/to/framework/Autoloader.php');
opcache_compile_file('/path/to/framework/Router.php');
// ... 更多文件然后在php.ini里设置:
opcache.enable=1
opcache.enable_cli=1
opcache.preload=/path/to/preload.php
opcache.preload_user=www-data这样,PHP-FPM启动的时候,就会把这些文件预加载到共享内存里,所有的worker进程都能直接用,不需要再加载了,性能提升很明显,尤其是对于框架类的文件,效果更好。
不过,预加载也有一些限制,比如预加载的文件不能在运行时修改,修改了需要重启PHP-FPM;预加载的文件不能有条件定义的类和函数;预加载会增加内存占用,因为这些文件一直驻留在内存里。所以,预加载适合那些不经常修改的、核心的框架文件和类库,不适合业务代码。
这个特性,对于我们这种大型项目来说,很有吸引力,能提升性能,我们迁移之后,准备测试一下预加载的效果,看看能提升多少性能。
7. FFI(Foreign Function Interface)
FFI是PHP 7.4的一个实验性特性,可以让PHP直接调用C语言的函数和使用C语言的数据结构,不需要写扩展。这样,PHP可以更方便地和C语言的库交互,性能也更好。
FFI的用法也比较简单,比如:
$ffi = FFI::cdef("
int printf(const char *format, ...);
", "libc.so.6");
$ffi->printf("Hello %s!\n", "world");这样,就直接调用了C语言的printf函数。FFI还可以定义C语言的结构体,使用C语言的指针,功能很强大。
不过,FFI是实验性的,而且有安全风险,因为可以直接调用C语言的函数,如果用不好,可能会导致PHP崩溃,甚至有安全漏洞。所以,FFI默认是禁用的,需要在php.ini里开启ffi.enable,而且建议只在可信的代码中使用。
对于大部分PHP开发者来说,FFI可能用不上,但是对于一些需要高性能、需要和C库交互的场景,FFI很有用。我们的项目暂时用不上FFI,但是了解一下还是有好处的。
8. 其他新特性
除了上面这些,PHP 7.4还有一些其他的新特性,比如:
- 数字字面量可以用下划线分隔,比如
1000000,更易读。 strip_tags()可以接受数组形式的允许标签。password_hash()支持了argon2i和argon2id的自定义参数。- 新增了
mbstrsplit()函数,把多字节字符串分割成数组。 - 新增了
getmangledobject_vars()函数,返回对象的属性,包括私有和受保护的。 - 弃用了一些旧的特性,比如没有括号的
include/require的嵌套、real类型、is_real()函数等。
这些新特性,虽然不是很重要,但是也能让开发更方便,代码更规范。
三、迁移过程中的兼容性问题
介绍完新特性,再说说我们迁移过程中遇到的兼容性问题,这些是迁移的时候需要注意的。
1. 类型属性的兼容性问题
类型属性是PHP 7.4的新特性,如果你的代码里用了类型属性,那么在PHP 7.3及以下的版本里会报错,因为不认识这个语法。所以,如果你的代码需要兼容旧版本的PHP,就不能用类型属性,或者用条件判断来处理。
我们的项目,迁移之后就只支持PHP 7.4了,所以可以放心用类型属性。但是,迁移的过程中,需要逐步给类加上类型属性,不能一下子全加上,因为有些属性的类型可能不明确,或者有历史问题,需要仔细检查。
还有一个问题,类型属性在声明的时候,如果没有默认值,那么属性的初始状态是uninitialized,访问未初始化的属性会抛出Error,而不是像之前那样返回null。所以,用了类型属性之后,要确保属性在访问之前已经初始化了,要么给默认值,要么在构造函数里初始化,不然会报错。
我们迁移的时候,就遇到了这个问题,有些类的属性声明了类型,但是没有初始化,访问的时候报错了,后来我们给这些属性加上了默认值,或者在构造函数里初始化,才解决。
2. 箭头函数的变量捕获问题
箭头函数会自动捕获外层的变量,但是是按值捕获的,不是按引用捕获的,所以在箭头函数里修改外层的变量,不会影响外层的值。比如:
$x = 1;
$fn = fn() => $x++; // 这里的$x是外层的副本,修改不影响外层
$fn();
echo $x; // 还是1如果需要按引用捕获,还是要用传统的闭包,用use (&$x)。
我们迁移的时候,有一些用闭包修改外层变量的代码,如果改成箭头函数,就会出问题,因为箭头函数是按值捕获的。所以,这种情况不能改成箭头函数,还是要用传统的闭包。
3. 数组展开运算符的兼容性问题
数组展开运算符,只能展开数组和Traversable对象,不能展开字符串或者其他类型,如果展开了非数组和非Traversable的类型,会抛出TypeError。之前,用array_merge的话,会把字符串转换成数组,虽然结果不对,但是不会报错。所以,迁移的时候,要确保展开的是数组或者Traversable对象,不然会报错。
还有,数组展开运算符不能展开字符串键的数组,只能展开数字键的数组,如果展开了字符串键的数组,会抛出Error。所以,如果数组有字符串键,还是要用array_merge。
我们迁移的时候,就遇到了这个问题,有一个数组有字符串键,用了展开运算符,结果报错了,后来改成了array_merge,才解决。
4. 弃用的特性
PHP 7.4弃用了一些旧的特性,这些特性在PHP 7.4里会抛出Deprecated警告,在PHP 8.0里会被移除。迁移的时候,要检查代码里有没有用到这些弃用的特性,及时修改。
主要弃用的特性有:
- 没有括号的
include/require的嵌套,比如include 'file.php' + 1,这种写法会被弃用,要加上括号。 real类型和isreal()函数,real是float的别名,已经弃用了,要用float和isfloat()。- 在父类的构造函数里调用
parent::__construct()的时候,如果父类没有构造函数,会抛出Deprecated警告,之前是静默的。 - 用
arraykeyexists()检查对象的属性,已经弃用了,要用property_exists()或者isset()。 - 大括号访问数组和字符串的偏移量,比如
$arr{0},已经弃用了,要用方括号$arr[0]。 convertcyrstring()函数,已经弃用了,要用mbconvertstring()或者iconv()。
我们迁移的时候,用静态分析工具扫描了一下代码,发现了几处用到了弃用的特性,比如大括号访问数组偏移量、is_real()函数等,都及时修改了,避免了警告。
5. 扩展的兼容性
PHP 7.4是一个大版本,有些扩展可能还不支持PHP 7.4,或者有兼容性问题。迁移的时候,要检查项目用到的所有扩展,确认是否支持PHP 7.4,如果不支持,要么等扩展更新,要么找替代方案,要么自己修改扩展。
我们的项目用到的扩展比较多,比如redis、memcached、imagick、xdebug、swoole等,我们检查了一下,大部分扩展都已经支持PHP 7.4了,只有少数几个扩展的旧版本不支持,升级到最新版本就好了。
还有一个问题,就是自定义扩展,如果项目里有自己写的PHP扩展,需要重新编译,适配PHP 7.4的API,因为PHP 7.4的扩展API有一些变化,有些旧的API被移除或者修改了。我们的项目没有自定义扩展,所以没有这个问题。
四、迁移步骤和经验
最后,说说我们的迁移步骤和经验,给大家参考。
1. 先在测试环境搭建PHP 7.4
迁移的第一步,是在测试环境搭建PHP 7.4的环境,和现有的PHP 7.2环境并存,这样可以在不影响现有环境的情况下,测试PHP 7.4的兼容性。
我们用的是Docker,很方便,拉一个PHP 7.4的镜像,安装需要的扩展,配置好环境,就可以测试了。如果不用Docker,也可以在服务器上编译安装PHP 7.4,和现有的PHP并存,用不同的端口或者socket。
2. 用静态分析工具扫描代码
搭建好环境之后,用静态分析工具扫描代码,检查兼容性问题。我们用的是PHPStan和Psalm,这两个工具都能检查类型错误、弃用的特性、潜在的bug等,能帮我们快速发现兼容性问题。
除了静态分析工具,还可以用PHP的-d errorreporting=EALL来运行代码,看看有没有警告和错误,尤其是Deprecated警告,能发现用到的弃用特性。
我们扫描之后,发现了几十处兼容性问题,大部分是弃用的特性,还有一些类型不匹配的问题,都一一修改了。
3. 跑单元测试和集成测试
修改完兼容性问题之后,跑单元测试和集成测试,看看有没有功能问题。我们的项目有比较完善的单元测试和集成测试,跑了一遍,发现了几个因为PHP版本差异导致的bug,比如数组排序的顺序变化、字符串处理的差异等,都修复了。
如果项目的测试覆盖不够,建议手动测试一下核心的功能,确保没有问题。
4. 性能测试和对比
功能没问题之后,做性能测试,对比PHP 7.2和PHP 7.4的性能,看看提升了多少,有没有性能下降的地方。
我们用的是ab和wrk做压力测试,测试了几个核心的接口,发现PHP 7.4的性能比PHP 7.2提升了大概5%到10%,不同的接口提升不一样,有的接口提升明显,有的几乎没变化。整体来说,性能还是有提升的,符合预期。
我们还测试了预加载的效果,开启预加载之后,性能又提升了大概5%左右,尤其是框架初始化的开销减少了很多,效果不错。
5. 灰度发布,逐步切换
测试没问题之后,不要一下子全量切换到PHP 7.4,要灰度发布,逐步切换。我们的做法是,先把少量的流量切到PHP 7.4,观察一段时间,看看有没有问题,监控错误率、响应时间、内存占用等指标,如果没问题,再逐步增加流量,最后全量切换。
灰度发布的过程中,要密切监控,如果发现问题,及时切回PHP 7.2,避免影响用户。我们灰度了大概一周,确认没问题之后,才全量切换到PHP 7.4。
6. 全量切换后,继续观察
全量切换之后,还要继续观察一段时间,看看有没有隐藏的问题,比如内存泄漏、性能下降、错误率升高等。我们全量切换之后,观察了两周,确认没有问题,才把PHP 7.2的环境下线。
全量切换之后,还可以逐步用上PHP 7.4的新特性,比如类型属性、箭头函数等,但是不要一下子全改,要逐步改,先改核心的、稳定的代码,避免引入新的bug。
五、踩坑记录
最后,说说我们迁移过程中踩的几个坑,给大家提个醒。
坑一:类型属性未初始化导致报错
前面提到过,类型属性如果没有默认值,初始状态是uninitialized,访问的时候会报错。我们有一个Model类,给属性加了类型,但是有些属性是在ORM里动态赋值的,没有在构造函数里初始化,访问的时候就报错了。后来我们给这些属性加上了默认值,或者改成了可空类型,给默认值null,才解决。
所以,用类型属性的时候,一定要确保属性在访问之前已经初始化了,要么给默认值,要么在构造函数里初始化,不要依赖动态赋值。
坑二:箭头函数按值捕获,修改外层变量不生效
我们有一处代码,用闭包来累计一个变量,闭包里修改外层的变量,用use (&$count)。迁移的时候,有人把它改成了箭头函数,结果发现变量没有累计,因为箭头函数是按值捕获的,修改的是副本,不影响外层。后来又改回了传统的闭包,才解决。
所以,箭头函数适合简单的、只读的表达式,如果需要修改外层的变量,还是要用传统的闭包,不要盲目改成箭头函数。
坑三:数组展开运算符不支持字符串键
我们有一处代码,合并两个数组,其中一个数组有字符串键,有人用了展开运算符,结果报错了,因为展开运算符不支持字符串键的数组。后来改成了array_merge,才解决。
所以,用数组展开运算符的时候,要确保数组是数字键的,如果有字符串键,还是要用array_merge。
坑四:预加载的文件有条件定义的类
我们测试预加载的时候,把一个框架的核心文件预加载了,但是这个文件里有条件定义的类,比如if (!class_exists('SomeClass')) { class SomeClass {} },结果预加载的时候报错了,因为预加载不支持条件定义的类。后来我们把这个文件从预加载里去掉了,才解决。
所以,预加载的文件,不能有条件定义的类和函数,也不能有副作用,只能是纯粹的类和函数定义,不然预加载会出问题。
坑五:扩展版本不兼容
我们有一个扩展,用的是旧版本,不支持PHP 7.4,升级到PHP 7.4之后,这个扩展加载失败,导致相关的功能都用不了。后来我们升级了扩展到最新版本,才解决。
所以,迁移之前,一定要检查所有用到的扩展,确认是否支持PHP 7.4,如果不支持,要提前升级或者找替代方案,不要等到迁移的时候才发现。
六、写在最后
PHP 7.4是一个很不错的版本,带来了很多实用的新特性,性能也有提升,值得升级。但是,迁移的时候一定要小心,做好测试,逐步切换,不要盲目全量上线,不然很容易出问题。
我们这次迁移,整体来说还是比较顺利的,虽然踩了几个坑,但是都及时解决了,全量切换之后,运行很稳定,性能也有提升,效果不错。
如果你也准备迁移到PHP 7.4,希望这篇文章能给你一些参考,让你少走弯路,少踩坑。如果有什么问题,也欢迎在评论区交流。
最后,期待PHP 8.0的到来,听说会有JIT、联合类型、命名参数等更多激动人心的新特性,PHP会越来越好的。
(注:本文基于2019年8月时PHP 7.4 beta版本的情况撰写,PHP 7.4正式发布于2019年11月28日,具体特性以正式版本为准。)
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录