PHP 7.4即将在本月底正式发布,带来了很多新特性和改进,今天来深入聊聊这些新特性和一些你可能不知道的技巧。

PHP,7.4,即将,在,本月底,正式,发布,了,作为,PHP,7,系列,的,最后,一个,小版本,PHP,7.4,带来,了,很多,新,特性,和,改进,性能,也,有,进一步,提升,很多,朋友,可能,已经,了解,了,一些,PHP,7.4,的,新,特性,比如,类型,属性,箭头,函数,预加载,等等,但是,可能,对,这些,特性,的,深入,用法,和,一些,技巧,还,不,太,了解。

我,自己,最近,一直在,关注,PHP,7.4,的,进展,也,试用,了,RC,版本,对,新,特性,有,一些,心得,和,体会,今天,这篇,文章,就,来,深入,聊聊,PHP,7.4,的,新,特性,和,一些,你,可能,不,知道,的,技巧。

一、类型属性:让代码更安全

类型属性,Typed Properties,是,PHP,7.4,最,重要,的,新,特性,之一,允许,在,类,的,属性,上,声明,类型,包括,public,protected,private,属性,都,可以,声明,类型,这,能,让,代码,更,安全,更,清晰,也,能,帮助,IDE,和,静态,分析,工具,更,好,地,理解,代码。

基本,用法,很,简单:

class User {
    public int $id;
    public string $name;
    public ?string $email;
    public array $roles;
}

这样,$id,只能,是,整数,$name,只能,是,字符串,$email,可以,是,字符串,或者,null,$roles,只能,是,数组,如果,赋值,类型,不对,会,直接,报,TypeError,错误,能,在,开发,阶段,就,发现,类型,错误,避免,很多,bug。

但是,类型属性,有,一些,细节,和,技巧,很多,人,可能,不,太,了解,下面,来说,说。

1. 类型属性没有默认值的话,初始状态是uninitialized

这,是,一个,很,重要,的,细节,很多,人,容易,踩,坑,在,PHP,7.4,之前,类,的,属性,如果,没有,默认值,会,默认,是,null,但是,在,PHP,7.4,中,如果,属性,声明,了,类型,但是,没有,默认值,也,没有,在,构造函数,里,赋值,那么,它,的,初始,状态,不是,null,而是,uninitialized,未初始化,访问,uninitialized,的,属性,会,报,Error,错误,而,不是,返回,null。

比如:

class User {
    public int $id;
}

$user = new User();
var_dump($user->id); // Error: Typed property User::$id must not be accessed before initialization

这,和,之前,的,行为,不一样,很多,人,容易,踩,坑,所以,声明,了,类型,的,属性,一定要,确保,在,使用,前,已经,初始化,了,可以,给,默认值,或者,在,构造函数,里,赋值,或者,用,isset,来,检查,是否,已经,初始化,了。

如果,你,希望,属性,默认,是,null,那么,要,显式,声明,为,可空,类型,并且,给,默认值,null:

class User {
    public ?int $id = null;
}

这样,$id,默认,就是,null,访问,也,不会,报错,了。

2. 可以用isset和empty检查uninitialized状态

虽然,访问,uninitialized,的,属性,会,报错,但是,用,isset,和,empty,检查,是,不会,报错,的,isset,对,uninitialized,的,属性,返回,false,empty,也,返回,true,所以,可以,用,isset,来,检查,属性,是否,已经,初始化,了:

class User {
    public int $id;
}

$user = new User();
var_dump(isset($user->id)); // false
$user->id = 1;
var_dump(isset($user->id)); // true

这,在,一些,场景,下,很,有用,比如,延迟,加载,属性,的,时候,可以,先,检查,是否,已经,初始化,了,再,访问。

3. 类型属性不支持callable和void类型

类型属性,支持,大部分,PHP,类型,包括,int,float,string,bool,array,object,iterable,self,parent,以及,类名,和,接口名,也,支持,可空,类型,?Type,但是,不,支持,callable,和,void,类型,因为,这,两个,类型,作为,属性,类型,没有,意义,callable,在,PHP,中,有,多种,形式,字符串,数组,闭包,等等,类型,检查,比较,复杂,所以,不,支持,void,只能,作为,函数,返回值,类型,不能,作为,属性,类型,所以,不要,在,属性,上,用,这,两个,类型,会,报,语法,错误。

4. 类型属性和继承的关系

类型属性,在,继承,的时候,有,一些,规则,子类,可以,重新,声明,父类,的,属性,但是,类型,必须,兼容,也就是,子类,的,属性,类型,必须,是,父类,属性,类型,的,子类型,或者,相同,不能,把,父类,的,int,属性,改成,string,这,会,报,致命,错误,而且,子类,不能,改变,属性,的,访问,控制,的,严格性,比如,父类,是,protected,子类,不能,改成,private,但是,可以,改成,public,这些,规则,和,方法,的,继承,规则,类似,保证,了,里氏,替换,原则。

还有,一个,细节,子类,重新,声明,父类,的,属性,时,如果,父类,的,属性,有,默认值,子类,可以,改变,默认值,但是,如果,父类,的,属性,是,const,或者,是,readonly,PHP,8.1,才,有,readonly,这里,不,讨论,就,不能,改变,了,这些,细节,在,使用,继承,的,时候,要,注意。

二、箭头函数:更简洁的闭包写法

箭头函数,Arrow Functions,也,叫,短闭包,Short Closures,是,PHP,7.4,另一个,很,受欢迎,的,新,特性,允许,用,更,简洁,的,语法,写,闭包,尤其是,只有,一个,表达式,的,简单,闭包,能,大大,简化,代码。

基本,用法:

// 传统闭包
$double = function($x) {
    return $x * 2;
};

// 箭头函数
$double = fn($x) => $x * 2;

是不是,简洁,了,很多,箭头函数,用,fn,关键字,参数,在,括号,里,然后,=>,后面,跟,一个,表达式,这个,表达式,的,值,就是,返回值,不需要,写,return,也,不需要,写,花括号,非常,简洁。

箭头函数,还有,一个,很,重要,的,特性,就是,自动,捕获,外层,变量,不需要,用,use,关键字,传统,闭包,如果,要,用,外层,变量,需要,显式,用,use,引入,而,箭头函数,自动,按值,捕获,外层,所有,变量,非常,方便:

$factor = 2;

// 传统闭包,需要use
$double = function($x) use ($factor) {
    return $x * $factor;
};

// 箭头函数,自动捕获
$double = fn($x) => $x * $factor;

这,在,写,回调,函数,的,时候,非常,方便,比如,arraymap,arrayfilter,等,函数,的,回调,用,箭头函数,能,简化,很多:

$numbers = [1, 2, 3, 4, 5];

// 传统写法
$squared = array_map(function($n) {
    return $n * $n;
}, $numbers);

// 箭头函数写法
$squared = array_map(fn($n) => $n * $n, $numbers);

简洁,了,很多,代码,也,更,易读。

但是,箭头函数,也,有,一些,限制,和,细节,很多,人,可能,不,太,了解,下面,来说,说。

1. 箭头函数只能有一个表达式,不能有多个语句

这,是,箭头函数,最大,的,限制,箭头函数,的,=>,后面,只能,跟,一个,表达式,不能,有,多个,语句,也,不能,有,花括号,所以,如果,你的,闭包,逻辑,比较,复杂,需要,多个,语句,那么,还是,得,用,传统,的,闭包,写法,箭头函数,只,适合,简单,的,单表达式,闭包,比如,简单,的,计算,转换,过滤,等等,不要,强行,用,箭头函数,写,复杂,逻辑,会,让,代码,变,得,难,读。

不过,虽然,只能,有,一个,表达式,但是,这个,表达式,可以,很,复杂,比如,嵌套,函数,调用,三元,运算符,等等,只要,是,一个,表达式,就,行:

$result = array_map(fn($x) => $x > 0 ? sqrt($x) : 0, $numbers);

这,也是,合法,的,虽然,表达式,有点,复杂,但是,还是,一个,表达式。

2. 箭头函数自动捕获变量是按值捕获,不是按引用

箭头函数,自动,捕获,外层,变量,但是,是,按值,捕获,的,也就是,捕获,的,是,变量,的,值,的,副本,在,箭头函数,里,改变,变量,的,值,不会,影响,外层,的,变量,而且,外层,变量,在,箭头函数,定义,之后,改变,也,不会,影响,箭头函数,里,的,值,因为,是,按值,捕获,的,定义,的,时候,就,把,值,复制,了,一份:

$x = 1;
$fn = fn() => $x;
$x = 2;
echo $fn(); // 输出1,不是2,因为是按值捕获

这,和,传统,闭包,的,use,按值,捕获,是,一样,的,如果,要,按引用,捕获,箭头函数,不,支持,只能,用,传统,闭包,加,use,&,的,方式,所以,如果你,需要,在,闭包,里,修改,外层,变量,那么,还是,得,用,传统,闭包。

3. 箭头函数也支持参数类型和返回类型

箭头函数,也,支持,参数,类型,声明,和,返回,类型,声明,和,普通,函数,一样:

$double = fn(int $x): int => $x * 2;

这样,参数,必须,是,整数,返回值,也,必须,是,整数,类型,不对,会,报,TypeError,能,让,代码,更,安全,在,重要,的,地方,建议,加上,类型,声明。

4. 箭头函数可以嵌套

箭头函数,也,可以,嵌套,也就是,一个,箭头函数,返回,另一个,箭头函数,在,函数式,编程,的,场景,下,很,有用:

$multiplier = fn($factor) => fn($x) => $x * $factor;
$double = $multiplier(2);
echo $double(5); // 输出10

这,是,柯里化,的,写法,用,箭头函数,能,很,简洁,地,实现,函数式,编程,的,一些,模式,很,方便。

三、预加载:大幅提升性能

预加载,Preloading,是,PHP,7.4,另一个,重要,的,新,特性,也是,对,性能,提升,最大,的,特性,之一,预加载,允许,在,PHP,启动,的,时候,把,一些,PHP,文件,加载,到,内存,里,并且,永久,缓存,这样,后续,的,请求,就,不,需要,再,加载,和,解析,这些,文件,了,能,大幅,提升,性能,尤其是,对于,框架,和,大型,项目,性能,提升,很,明显。

预加载,的,原理,是,利用,OPcache,的,共享,内存,在,PHP,启动,的,时候,把,指定,的,PHP,文件,解析,编译,成,字节码,存,在,共享,内存,里,并且,永久,缓存,不,会,被,清除,这样,每个,请求,都,能,直接,用,这些,已经,编译,好,的,字节码,不,需要,再,加载,解析,编译,了,省去,了,很多,开销,能,提升,20%,到,50%,的,性能,对于,框架,项目,提升,更,明显,因为,框架,有,很多,文件,每次,请求,都,要,加载,预加载,之后,就,不用,了。

配置,预加载,也,很,简单,在,php.ini,里,配置,两个,选项:

opcache.preload = /path/to/preload.php
opcache.preload_user = www-data

opcache.preload,指定,预加载,的,入口,文件,这个,文件,里,定义,要,预加载,哪些,文件,opcache.preload_user,指定,运行,预加载,的,用户,一般,是,Web服务器,的,运行,用户,比如,www-data,nginx,apache,等等,因为,预加载,是,在,PHP,启动,的,时候,执行,的,需要,指定,用户,权限。

然后,在,preload.php,里,定义,要,预加载,的,文件,可以,用,opcachecompilefile,函数,来,编译,指定,的,文件,也,可以,用,require,或者,include,来,加载,文件,一般,推荐,用,opcachecompilefile,因为,它,只,编译,不,执行,不会,有,副作用,更,安全:

<?php
// 预加载所有框架核心文件
$files = glob(__DIR__ . '/src/**/*.php');
foreach ($files as $file) {
    opcache_compile_file($file);
}

这样,PHP,启动,的,时候,就,会,把,src,目录,下,所有,PHP,文件,都,编译,好,存,在,共享,内存,里,后续,请求,直接,用,不用,再,加载,了。

但是,预加载,也,有,一些,限制,和,注意,事项,很多,人,可能,不,太,了解,下面,来说,说。

1. 预加载只能在PHP-FPM或Apache模块模式下使用,CLI模式下不生效

预加载,是,基于,共享,内存,的,需要,PHP,进程,常驻,内存,才能,共享,所以,只能,在,PHP-FPM,或者,Apache,模块,模式,下,使用,CLI,模式,下,每次,执行,完,就,退出,了,没有,共享,内存,所以,预加载,不,生效,也,没有,意义,所以,如果你,用,的,是,CLI,模式,跑,脚本,不用,考虑,预加载,预加载,主要,是,给,Web,应用,用,的。

2. 预加载的文件不能有副作用,或者副作用要可控

如果,用,require,或者,include,来,预加载,文件,那么,这些,文件,里,的,代码,会,在,预加载,的,时候,执行,也就是,PHP,启动,的,时候,执行,如果,文件,里,有,副作用,比如,定义,常量,修改,全局,变量,发送,输出,等等,可能,会,有,问题,因为,这些,副作用,会,在,所有,请求,之前,就,执行,了,可能,影响,后续,请求,所以,一般,推荐,用,opcachecompilefile,来,预加载,它,只,编译,文件,不,执行,不会,有,副作用,更,安全,但是,opcachecompilefile,不会,处理,文件,之间,的,依赖,关系,如果,文件,有,依赖,需要,确保,依赖,的,文件,也,被,预加载,了,不然,可能,有,问题。

3. 预加载后,文件修改不会自动生效,需要重启PHP

这,是,预加载,一个,很,重要,的,注意,事项,预加载,的,文件,是,在,PHP,启动,的,时候,编译,好,存,在,共享,内存,里,的,永久,缓存,所以,如果你,修改,了,预加载,的,文件,不会,自动,生效,因为,共享,内存,里,的,还是,旧,的,字节码,需要,重启,PHP-FPM,或者,Web服务器,才能,生效,这,在,开发,环境,下,很,不,方便,所以,开发,环境,不,建议,开启,预加载,预加载,主要,用,在,生产,环境,生产,环境,代码,不,经常,修改,每次,发布,重启,PHP,就,行,了,影响,不大,但是,开发,环境,经常,改,代码,开启,预加载,会,很,麻烦,所以,开发,环境,不要,开。

4. 不是所有文件都适合预加载,要选择性预加载

虽然,预加载,能,提升,性能,但是,也,不是,所有,文件,都,适合,预加载,预加载,的,文件,会,一直,占,着,共享,内存,如果,预加载,了,很多,不,常用,的,文件,会,浪费,内存,而且,预加载,的,文件,越多,PHP,启动,时间,越,长,所以,要,选择性,地,预加载,只,预加载,那些,每个,请求,都,会,用到,的,核心,文件,比如,框架,核心,类,基础,类,等等,不,常用,的,文件,不用,预加载,让,OPcache,正常,缓存,就,行,了,这样,能,在,性能,和,内存,之间,取得,平衡。

一般,来说,框架,都,会,提供,预加载,的,配置,或者,推荐,的,预加载,文件,列表,比如,Symfony,Laravel,都,有,相关,的,文档,和,配置,可以,参考,框架,的,建议,来,配置,预加载,不用,自己,瞎,搞。

四、其他实用新特性

除了,上面,三个,比较,大,的,新,特性,PHP,7.4,还有,一些,比较,小,但是,很,实用,的,新,特性,下面,来,介绍,几个。

1. 空合并赋值运算符

空合并,赋值,运算符,??=,是,一个,很,实用,的,小,特性,允许,在,变量,为,null,的,时候,才,赋值,否则,不,赋值,简化,了,很多,常见,的,写法。

比如,以前,我们,经常,写:

if (!isset($array['key'])) {
    $array['key'] = 'default';
}

或者:

$array['key'] = $array['key'] ?? 'default';

现在,用,??=,可以,更,简洁:

$array['key'] ??= 'default';

意思,是,如果,$array['key'],不存在,或者,为,null,就,赋值,为,'default',否则,保持,原值,不变,非常,简洁,也,很,易读,在,设置,默认值,的,场景,下,很,常用。

2. 数字字面量中的下划线

PHP,7.4,允许,在,数字,字面量,中,使用,下划线,来,分隔,数字,提高,可读性,比如,很大,的,数字,以前,写,一长串,0,很,难,数,现在,可以,用,下划线,分隔,就,清楚,多,了:

$price = 1_000_000; // 1000000,一百万
$pi = 3.141_592_653; // 圆周率

下划线,只是,为了,可读性,PHP,解析,的,时候,会,忽略,下划线,不,影响,数值,这,在,写,大,数字,的,时候,很,实用,能,减少,写错,数字,的,概率,也,更,易读。

3. 异常中的Throwable接口

PHP,7.4,对,异常,处理,做,了,一些,改进,现在,Exception,和,Error,都,实现,了,Throwable,接口,而且,可以,在,catch,中,用,Throwable,来,捕获,所有,异常,和,错误,这,在,PHP,7,就,有,了,但是,PHP,7.4,做,了,一些,完善,现在,可以,更,方便,地,捕获,所有,可抛出,的,对象:

try {
    // 代码
} catch (Throwable $e) {
    // 捕获所有异常和错误
    echo $e->getMessage();
}

这,在,一些,场景,下,很,有用,比如,全局,异常,处理,日志,记录,等等,能,捕获,所有,错误,和,异常,不,会,遗漏。

4. 数组展开运算符

PHP,7.4,支持,在,数组,中,使用,展开,运算符,...,来,把,一个,数组,的,元素,展开,到,另一个,数组,里,这,在,以前,只能,用,在,函数,参数,中,现在,也,能,用,在,数组,中,了,很,方便:

$array1 = [1, 2, 3];
$array2 = [0, ...$array1, 4, 5]; // [0, 1, 2, 3, 4, 5]

这样,能,很,方便,地,合并,数组,比,array_merge,更,简洁,也,更,易读,而且,能,在,任意,位置,展开,很,灵活,不过,注意,只能,展开,数字,键,的,数组,字符串,键,的,数组,不能,用,展开,运算符,会,报,错误,因为,字符串,键,可能,冲突,所以,这个,特性,主要,用,在,数字,索引,数组,上。

五、升级建议和注意事项

最后,来说,说,升级,到,PHP,7.4,的,建议,和,注意,事项。

1. 先检查兼容性,再升级

升级,PHP,版本,之前,一定要,先,检查,代码,的,兼容性,PHP,7.4,虽然,是,PHP,7,系列,的,小版本,大部分,代码,都,能,兼容,但是,也,有,一些,不兼容,的,变更,比如,一些,废弃,的,特性,被,移除,了,一些,函数,的,行为,改变,了,等等,如果,代码,里,用,了,这些,特性,升级,后,可能,会,出,问题,所以,升级,前,一定要,先,检查,兼容性,可以,用,PHP_CodeSniffer,的,PHPCompatibility,标准,来,扫描,代码,检查,兼容性,也,可以,在,测试,环境,先,升级,测试,没问题,了,再,在,生产,环境,升级,不要,直接,在,生产,环境,升级,容易,出,问题。

2. 逐步使用新特性,不要一下子全改

升级,到,PHP,7.4,之后,不要,一下子,把,所有,代码,都,改成,用,新,特性,建议,逐步,使用,先,在,新,写,的,代码,里,用,新,特性,然后,慢慢,重构,旧,代码,这样,风险,小,也,能,让,团队,慢慢,适应,新,特性,的,写法,而且,新,特性,也,要,根据,实际,情况,使用,不要,为了,用,而,用,比如,类型属性,虽然,好,但是,也,要,考虑,代码,的,实际,情况,有些,属性,类型,不,确定,的,就,不要,强行,加,类型,箭头函数,虽然,简洁,但是,复杂,逻辑,还是,用,传统,闭包,更,好,要,合理,使用,新,特性,发挥,它们,的,优势,避免,滥用。

3. 开启OPcache和预加载,提升性能

升级,到,PHP,7.4,之后,一定要,开启,OPcache,并且,配置,预加载,这样,能,充分,发挥,PHP,7.4,的,性能,优势,OPcache,能,缓存,字节码,避免,每次,请求,都,重新,解析,编译,预加载,能,进一步,把,核心,文件,永久,缓存,提升,性能,这,两个,配置,都,很,简单,但是,性能,提升,很,明显,一定要,配置,当然,预加载,只,在,生产,环境,开启,开发,环境,不要,开,前面,说,过,了。

4. 关注废弃特性,提前准备PHP 8

PHP,7.4,是,PHP,7,系列,的,最后,一个,小版本,下一个,大版本,就是,PHP,8,了,PHP,8,会,有,很多,大,的,变更,也,会,移除,很多,废弃,的,特性,所以,在,使用,PHP,7.4,的,时候,要,关注,哪些,特性,被,废弃,了,尽量,不要,用,废弃,的,特性,提前,为,升级,PHP,8,做,准备,这样,以后,升级,PHP,8,的,时候,会,更,顺利,PHP,7.4,的,废弃,特性,在,官方,文档,里,有,列表,可以,去,看看,尽量,避免,使用。

六、写在最后

PHP,7.4,是,一个,很,优秀,的,版本,带来,了,很多,实用,的,新,特性,和,性能,提升,类型属性,让,代码,更,安全,更,清晰,箭头函数,让,闭包,更,简洁,预加载,大幅,提升,性能,还有,空合并,赋值,数字,下划线,数组,展开,等,小,特性,也,很,实用,这些,新,特性,能,让,我们,写,代码,更,高效,更,愉快,也,能,让,代码,质量,更,高,性能,更,好。

当然,新,特性,也,要,合理,使用,不要,滥用,要,根据,实际,情况,选择,合适,的,特性,发挥,它们,的,优势,而且,升级,之前,要,做好,兼容性,检查,和,测试,确保,代码,能,正常,运行,再,升级,不要,贸然,升级,出,问题。

PHP,这几年,发展,很,快,从,PHP,7,到,PHP,7.4,再,到,即将,来,的,PHP,8,每次,更新,都,带来,很多,新,特性,和,性能,提升,PHP,已经,不是,以前,那个,被,人,吐槽,的,语言,了,越来越,现代,越来越,强大,作为,PHP,开发者,我们,要,跟上,PHP,的,发展,学习,新,特性,用,新,特性,写,更,好,的,代码,也,要,为,未来,的,版本,升级,做,准备,这样,才能,不,被,淘汰,写,出,更,优秀,的,PHP,应用。

希望,这篇,文章,能,给,使用,PHP,的,朋友,一些,参考,和,启发,帮,大家,更,好,地,理解,和,使用,PHP,7.4,的,新,特性,也,欢迎,大家,在,评论,区,分享,你们,使用,PHP,7.4,新,特性,的,经验,和,心得,或者,有,什么,问题,也,可以,在,评论,区,问,我,一起,交流,一起,进步。

最后,希望,PHP,发展,得,越来越,好,也,希望,我们,都,能,在,PHP,的,道路,上,不断,学习,不断,进步,写,出,更,优秀,的,代码,做,出,更,优秀,的,产品。