前言

距离 PHP 8 发布已经有一年多了,这个版本是 PHP 语言的主版本更新,包含了很多新功能与优化项,并改进了类型系统、错误处理,目前已经迭代到 PHP 8.0.10 版本。

由于更新的内容较多,本文仅介绍部分特性,完整内容可以去官网进行了解。

注解

注解是非常强大的一项功能,可以通过注解的方式减少很多配置,以及实现很多非常方便的功能,比如定义路由、AOP、自动注入等。

大多数框架都是通过反射解析 PHPDoc 的方式实现注解功能,比如我们现在用的 Hyperf 框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @Controller()
*/
class UserController
{
/**
* @RequestMapping(path="index", methods="get,post")
*/
public function index(RequestInterface $request)
{
...
}
}

PHP 社区显然也发现了注解对于现代编程语言的重要性,所以在 PHP 8 中从语言层面实现了注解,使得代码中的声明部分都可以添加结构化、机器可读的元数据,而非 PHPDoc 声明。

1
2
3
4
5
6
7
8
9
#[Controller]
class UserController
{
#[RequestMapping(path: "index", methods: "get,post")]
public function index(RequestInterface $request)
{
...
}
}

可以看到,这里使用的是 #[],为什么没有继续沿用 PHPDoc 中 @ 的方式?

因为 @ 在 PHP 中已经是一个有意义(抑制错误)的符号,不能再用来作为注解标识。

一开始是使用 <<ExampleAttribute>> 作为注解,由于吐槽的人太多,后来就改成了 #[ExampleAttribute]

命名参数

在 PHP 8 之前,调用函数时参数的顺序是固定的,并且不能跳过默认值进行传参。

  • 如果不按照函数声明的参数顺序进行传参,那么在函数中接收到的参数值也是不正确的,假如声明了参数类型,还会提示参数类型错误。
  • 当函数参数中有参数存在默认值,我们想要修改该参数后面的参数时,则必须要传入默认值才能达到目的,不能省略掉默认值。

命名参数允许直接以任意顺序使用参数名称进行传参,并且可以跳过某些默认值。

举个例子,htmlspecialchars 函数的原型如下:

1
2
3
4
5
6
htmlspecialchars(
string $string,
int $flags = ENT_COMPAT | ENT_HTML401,
string $encoding = ini_get("default_charset"),
bool $double_encode = true
): string

在 PHP 7 中使用该函数时,假如我们只想让 $double_encode 为 false,则必须传入 $flags 和 $encoding 默认值,像下面这样:

1
2
3
$val = '<b>hello</b>';

htmlspecialchars($val, ENT_COMPAT | ENT_HTML401, ini_get("default_charset"), false);

有了命名参数之后,我们可以忽略 $flags 和 $encoding 参数,仅传入 $string 和 $double_encode,并且可以随意调换参数顺序,下面这几种使用方式都是可以的。

1
2
3
4
5
6
7
8
$val = '<b>hello</b>';

// 忽略 $flags 和 $encoding 参数
htmlspecialchars($val, double_encode: false);
// 调换入参顺序
htmlspecialchars(double_encode: false, string: $val);
// 覆盖 $flags 的默认值
htmlspecialchars(double_encode: false, string: $val, flags: ENT_COMPAT);

联合类型

PHP 7.4 版本增加了类型属性,强化了 PHP 的类型系统,使得我们可以在类中声明属性的类型。

1
2
3
4
5
6
7
8
9
class Number 
{
private int $number;

public function __construct(int $number)
{
$this->number = $number;
}
}

但是该版本不支持联合类型,比如当 $number 既是 int 类型又是 float 类型时,就不能使用类型属性,只能通过 PHPDoc 注释的方式声明。

1
2
3
4
5
6
7
8
9
10
11
12
class Number 
{
/**
* @var float|int
*/
private $number;

public function __construct($number)
{
$this->number = $number;
}
}

这样就又回到了以前弱类型的老路子,失去了类型属性的作用。

所以 PHP 8 引入了联合类型,使得我们可以在类属性及参数签名中声明多种类型信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Number 
{
private float|int $number;

public function __construct(float|int $number)
{
$this->number = $number;
}

public function getNumber(): int|float
{
return $this->number;
}
}

联合类型带来的好处:

  • 类型是强制要求的,可以及早发现错误。
  • 因为是强制要求的,所以类型信息不太可能会出现遗漏的情况。
  • 可以通过反射获取类型信息。
  • 语法比起 PHPDoc 注释简洁很多。

更符合逻辑的比较

在旧版本的 PHP 中,如果用数字跟字符串进行非严格比较,会得到一些令人困惑的结果:

1
2
3
4
var_dump(0 == ""); // true
var_dump(0 == "foo"); // true
var_dump(42 == "42foo"); // true
var_dump(in_array(0, ["foo", "bar"])); // true

导致这一结果是因为:非严格比较运算符的字符串和数字之间的比较,是将字符串转为数字,然后对整数或浮点数进行比较。

在上面的示例中,"""foo" 会被转换为 0,"42foo" 会被转换为 42,然后在跟左侧的值进行比较,所以结果为 true。

PHP 8 比较数字字符串时,会按数字进行比较。不是数字字符串时,将数字转化为字符串,按字符串比较。

比如 42 == "42",会直接按照数字进行比较,如果是 42 == "42foo",由于 "42foo" 不是数字字符串,所以会将 42 转为 "42" 字符串,然后再进行比较。

所以在 PHP 8 中,上面示例的结果全部是 false。

统一内部函数类型错误

在调用 PHP 函数时,如果传入的参数解析失败或者缺少参数,PHP 将会提示 Warning 并继续运行:

1
2
3
4
5
6
7
8
9
10
strlen([]); // Warning: strlen() expects parameter 1 to be string, array given
array_chunk([], -1); // Warning: array_chunk(): Size parameter expected to be greater than 0
array_filter();

echo "hello";

//Warning: strlen() expects parameter 1 to be string, array given in /in/K0adM on line 2
//Warning: array_chunk(): Size parameter expected to be greater than 0 in /in/K0adM on line 3
//Warning: array_filter() expects at least 1 parameter, 0 given in /in/K0adM on line 4
//hello

有些“机智”的朋友会使用 error_reporting(E_ALL ^ E_WARNING) 来屏蔽异常,让程序能够继续正常运行。

就导致了函数虽然“正常”地返回了结果,但是结果不一定是正确的,所以我们就必须在代码里面对函数的结果进行校验。

PHP 8 针对这个问题进行了改进,统一了内部函数类型错误,现在大多数内部函数在参数验证失败时抛出 Error 级异常。

1
2
3
4
5
6
Fatal error: Uncaught TypeError: strlen(): Argument #1 ($str) must be of type string, array given in /in/F26cS:3
Stack trace:
#0 {main}
thrown in /in/F26cS on line 3

Process exited with code 255.

由此,我们也可以看出 PHP 社区的目的,避免通过返回值判断异常情况,而是使用抛出异常的方式。

即时编译

PHP 8 引入了两个即时编译引擎,Tracing JIT 和 Function JIT,前者更具有潜力,它在综合基准测试中显示了三倍的性能, 并在某些长时间运行的程序中显示了 1.5-2 倍的性能改进。

PHP 8 的 JIT 是在 Opcache 优化的基础之上,结合 Runtime 的信息再次优化,直接生成机器码。

下面是 Opcache 的流程示意图:

最后

推荐一个网站:https://3v4l.org/,支持300多个版本的PHP在线运行代码,可以很方便地在线调试各个版本的差异。

参考阅读