Yesccx Blog

laravel9及PHP8的相关新特性(一)

PHP新特性整理

Lavavel6.x起对PHP的版本最低要求是7.2,但可能在日常开发中还是以PHP5.x的一些思想去开发,很多新特性或比较实用的语法都没用上,所以这里整理了些PHP5.xPHP8.x中可能用的上的新特性,更多详情可以查阅PHP手册下的版本迁移记录。

PHP5.6.x => PHP7.0.x

完整更新记录 https://www.php.net/manual/zh/migration70.php

异常机制调整

引自 https://www.php.net/manual/zh/language.errors.php7.php

PHP7.x起所有内部异常类都实现了Throwable接口(但我们无法直接去implements实现该接口,只能通过继承PHP现有的异常类),同时PHP运行期间的大多数错误都以Error异常抛出,Error异常是所有内部异常的基类,所以当想要捕获这些由PHP内部抛出的异常时可以在catch中声明捕获Error异常或是顶级的Throwable接口

同时作为用户级的异常基类Exception异常,一般的框架都有相应的子异常类实现,这样就很好的区分了捕获的异常是内部异常还是由业务代码自行抛出的用户级异常。

常见的异常捕获场景如下:

1try {
2    // call ...
3} catch (\Exception $e) [
4    // ...长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3长长的标题3
5}

但是这种方式不能捕获PHP内部的一些 类型错误 等异常,如下:

1try {
2    $a = 1;
3    $b = $a / 0;
4} catch (\Exception $e) {
5    // 这里捕获不到
6}
7
8// 实际上PHP将抛出 DivisionByZeroError 异常

此时如果想正确的捕获异常应该:

 1// 方式1
 2try {
 3    $a = 1;
 4    $b = $a / 0;
 5} catch (\Throwable $e) {
 6    var_export('发生错误');
 7}
 8
 9// 方式2(可以是更具体的异常类)
10try {
11    $a = 1;
12    $b = $a / 0;
13} catch (\Error $e) {
14    var_export('发生错误');
15}

标量类型声明

引自 https://www.php.net/manual/zh/migration70.new-features.php

PHP7.0.x及之后的版本中,扩充了可用于声明的标量类型如intstring等,同时在这之后也支持了对函数的返回值做类型声明,示例如下:

 1<?php
 2
 3class User
 4{
 5    /**
 6     * 用户名
 7     * PS: PHP7.4之后可以直接在这里声明类成员属性的类型
 8     *
 9     * @var string
10     */
11    protected string $username = '';
12
13    /**
14     * 获取用户名
15     * PS: 此处声名了方法参数及返回值类型
16     *
17     * @param int $id 用户id
18     * @return string 用户名
19     */
20    public function getUsername(int $id): string
21    {
22        return $this->username;
23    }
24
25}

同时建议养成类型声明的习惯,能显著提高PHP代码的可读性,配合IDE能够在编码阶段就发现一些明显的类型错误。

严格模式

引自 https://www.php.net/manual/zh/language.types.declarations.php#language.types.declarations.strict

当一个函数声明接收一个int值,调用者却传递了一个float值,此时PHP会尝试强转将float转为int但不会报错,可能有些IDE会提示传参错误但PHP在运行期间却不会报错,当类似的情况出现在业务代码中时,可能会引发业务逻辑错误。

所以为了避免这种情况发生,建议在每个PHP文件中都声明强制开启严格模式,开启严格模式后方法只能接受与其声明类型完全匹配的值。

 1<?php
 2
 3declare(strict_types=1);
 4
 5function sum(int $a, int $b): int
 6{
 7    return $a + $b;
 8}
 9
10// 正常
11sum(1, 2);
12
13// 报错
14sum(1.5, 2.5);

严格模式和非严格模式还有更多区别,可以自行查看文档比较

null合并运算符

引自 https://www.php.net/manual/zh/migration70.new-features.php#migration70.new-features.null-coalesce-op

相当于三元表达式的简写,日常使用到的频率较高,有点类似PHP5.3出的?:写法。

 1<?php
 2
 3// ?? 的写法,以下两者等价
 4$username = isset($user['name']) ? $user['name'] : '匿名';
 5$username = $user['name'] ?? '匿名';
 6
 7// 对比PHP5.3发布的 ?: 的写法,以下两者等价
 8$username = !empty($user['name']) ? $user['name'] : '匿名';
 9$username = $user['name'] ?: '匿名';
10
11// 实际开发中可能的例子
12$article = Article::query()->with('cover')->find(1);
13$coverUrl = $article->cover->file_url ?? '';

太空般操作符

引自 https://www.php.net/manual/zh/migration70.new-features.php#migration70.new-features.spaceship-op

太空船操作符用于比较两个表达式。如下当$a小于、等于或大于$b时它分别返回-1、0或1,通常可以用在自定义排序上。

 1<?php
 2
 3// 整数
 4echo 1 <=> 1; // 0
 5echo 1 <=> 2; // -1
 6echo 2 <=> 1; // 1
 7
 8// 用作排序
 9$userList = [
10    ['name' => '张三', 'age' => 30],
11    ['name' => '李四', 'age' => 20],
12];
13
14usort($userList, function ($a, $b) {
15    return $a['age'] <=> $b['age'];
16});
17
18?>

匿名类

引自 https://www.php.net/manual/zh/migration70.new-features.php#migration70.new-features.anonymous-classes

有点类似匿名函数的概念,可以利用匿名类实例化出对象(支持类的一些特性,如继承等),实际编码中使用场景不多,可能一些框架底层会有使用到。

 1<?php
 2
 3class Foo
 4{
 5    public function fooInfo()
 6    {
 7        echo 'foo info';
 8    }
 9}
10
11$bar = new class extends Foo
12{
13    public function barInfo()
14    {
15        echo 'bar info';
16    }
17};
18
19$bar->fooInfo();
20
21$bar->barInfo();

PHP7.0.x => PHP7.1.x

完整更新记录 https://www.php.net/manual/zh/migration71.php

可为空(Nullable)类型

引自 https://www.php.net/manual/zh/migration71.new-features.php#migration71.new-features.nullable-types

现在可以声明参数或返回值的类型为null,只需要在类型前加上一个?即可。 当启用这个特性时,传入的参数或者函数返回的结果要么是给定的类型,要么是null

1<?php
2
3function testReturn(?string $name = null): ?string
4{
5    return $name;
6}

Void函数

引自 https://www.php.net/manual/zh/migration71.new-features.php#migration71.new-features.void-functions

可以声明函数的返回值为void,表示函数没有函数值,但当尝试获取返回值时会得到null。

1<?php
2
3function test(): void
4{
5
6}
7
8echo test(); // null

短数组语法及增强

引自 https://www.php.net/manual/zh/migration71.new-features.php#migration71.new-features.support-for-keys-in-list

PHPlist()语法有点类似Javscript ES6中的解构语法糖,用来提取数组中的元素同时赋值给多个变量,PHP7.1.x中新增了短数组语法[]与指定键名特性,使用频率也较高。

短数组语法:

1<?php
2
3// 以下两者等价
4list($a, $b) = [0, 1];
5[$a, $b] = [0, 1]; // $a = 0 ,$b = 1

指定键名的情况:

1<?php
2
3// 以下两者等价
4list('age' => $a, 'username' => $b) = ['username' => '张三', 'age' => 20];
5['age' => $a, 'username' => $b] = ['username' => '张三', 'age' => 20]; // $a = 20 ,$b = 张三

实际使用场景:

 1<?php
 2
 3// 场景1(foreach):
 4$userList = [
 5    ['id' => 1, 'name' => '张三'],
 6    ['id' => 2, 'name' => '李四'],
 7];
 8
 9foreach ($userList as $index => ['id' => $id, 'name' => $name]) {
10    var_export($id . $name);
11}
12
13// 场景2(函数返回值):
14function userInfo(): array
15{
16    return ['id' => 1, 'name' => '张三'];
17}
18['id' => $id, 'name' => $name] = userInfo();

多异常捕获处理

引自 https://www.php.net/manual/zh/migration71.new-features.php#migration71.new-features.mulit-catch-exception-handling

以往的版本中,try catch捕获异常时一个catch中只能声明一个异常类,在PHP7.1.x版本之后得到了增强,可以通过管道符|分隔来声明捕获多个异常类,这也比较实用。

1<?php
2
3try {
4    // run...
5} catch (FooException | BarException $e) {
6    // handler...
7}

支持为负的字符串偏移量

引自 https://www.php.net/manual/zh/migration71.new-features.php#migration71.new-features.support-for-negative-string-offsets

现在所有支持偏移量的字符串操作函数 都支持接受负数作为偏移量,包括通过[]或{}操作字符串下标。在这种情况下,一个负数的偏移量会被理解为一个从字符串结尾开始的偏移量,可以理解为倒着取。

1<?php
2
3var_dump("abcdef"[-2]); // e
4var_dump(strpos("aabbcc", "b", -3)); // 3

PHP7.1.x => PHP7.2.x

完整更新记录 https://www.php.net/manual/zh/migration72.php

新的对象类型

引自 https://www.php.net/manual/zh/migration72.new-features.php#migration72.new-features.object-type

新的类型object,用于参数及函数返回值的类型声明,如函数的返回值可能是好几个类之间的一个实例对象时,可以声明为object,一般使用场景不多,通常也不太建议使用,非特殊情况下建议声明为具体的类或拆分代码。

1<?php
2
3function test(object $obj) : object
4{
5    return new SplQueue();
6}
7
8test(new StdClass());

允许重写抽象方法(Abstract method)

引自 https://www.php.net/manual/zh/migration72.new-features.php#migration72.new-features.abstract-method-overriding

当一个抽象类继承于另外一个抽象类时,继承后的抽象类可以重写被继承的抽象类的抽象方法,使用场景不多,而且有一定限制,具体参照文档。

PHP7.2.x => PHP7.3.x

完整更新记录 https://www.php.net/manual/zh/migration73.php

这个版本的更新没有太多实用性的新特性,具体参考文档

PHP7.3.x => PHP7.4.x

完整更新记录 https://www.php.net/manual/zh/migration74.php

类成员属性类型声明

引自 https://www.php.net/manual/zh/migration74.new-features.php#migration74.new-features.core.typed-properties

可以在定义类成员属性时,声明成员属性的类型。

 1<?php
 2
 3class User
 4{
 5    /**
 6     * id
 7     * PS: 指明了成员变量类型为int
 8     *
 9     * @var int
10     */
11    public int $id;
12}

箭头函数

引自 https://www.php.net/manual/zh/migration74.new-features.php#migration74.new-features.core.arrow-functions

箭头函数提供了一种更简洁的方式定义匿名函数,有点类似Javascript ES6中的箭头函数写法。在PHP中,匿名函数内部要使用外部上下文中的变量时需要use声明,当使用箭头函数语法时,可以省去use,示例如下:

 1<?php
 2
 3// 以下两种写法等价
 4
 5// 写法1
 6$b = 1;
 7$sum1 = function (int $a) use ($b): int {
 8    return $a + $b;
 9};
10
11// 写法2
12$b = 1;
13$sum2 = fn (int $a): int => $a + $b;

实际使用场景:

1$groupId = $request->input('group_id', 0);
2
3$list = Member::query()
4    ->when(!empty($groupId), fn ($query) => $query->where('group_id', $groupId))
5    ->get();
 1<!doctype html>
 2<html lang="en">
 3<head>
 4  <meta charset="utf-8">
 5  <title>Example HTML5 Document</title>
 6</head>
 7<body>
 8  <p>Test</p>
 9</body>
10</html>

但普通函数相较于箭头函数而言有许多限制,比如只能写一条表达式等,所以在大多数情况下还是得用匿名函数。

空合并运算符赋值

引自 https://www.php.net/manual/zh/migration74.new-features.php#migration74.new-features.core.null-coalescing-assignment-operator

PHP7.0版本中发布了null合并运算符特性,是一种三元运算判断的简写方式,而空合并运算符赋值在编写形式及效果上也很类似,为一个不存在或为null的变量赋一个值。

 1<?php
 2
 3// 方式1
 4$array['key'] ??= computeDefault();
 5
 6// 方式2 等价
 7if (!isset($array['key'])) {
 8    $array['key'] = computeDefault();
 9}
10
11// 深层次赋值也可以
12$a['b']['c']['d'] = 1;

有了这种语法后,一般的null合并运算符操作相关代码可以再度精简,实际的案例如下:

 1<?php
 2
 3// 当获取不到应用的配置信息时,指定默认值
 4
 5// before
 6$app = AppService::getInfo(1);
 7$app['config'] = $app['config'] ?? [ 'status' => 1 ];
 8
 9// after
10$app = AppService::getInfo(1);
11$app['config'] ??= [ 'status' => 1 ];

数组展开操作

引自 https://www.php.net/manual/zh/migration74.new-features.php#migration74.new-features.core.unpack-inside-array

数组展开操作有点类似Javascript ES6中的展开运算符。这种写法的行为与数组array_splice函数相似,在数组的某个位置中插入新的数组。

 1<?php
 2
 3// array_splice写法
 4$parts = ['apple', 'pear'];
 5$fruits = ['banana', 'orange', 'watermelon'];
 6array_splice($fruits, 2, 0,$parts);
 7
 8// 数组展开操作写法
 9$parts = ['apple', 'pear'];
10$fruits = ['banana', 'orange', ...$parts, 'watermelon'];

OPcache Preloading(预加载)

引自 https://www.php.net/manual/zh/migration74.new-features.php#migration74.new-features.opcache

用于提升php的性能。如通过相关配置,可以在启动php-fpm时将定义好的PHP文件提前加载到进程内存中,从而减少后续请求的开销,如我们提前加载了一个带有test函数的PHP文件,在之后执行的PHP代码中可以直接调用这个test函数,体验上有点类似在调用php内置函数。

PHP7.4.x => PHP8.0.x

完整更新记录 https://www.php.net/manual/zh/migration80.php

命名参数

引自 https://www.php.net/manual/zh/functions.arguments.php#functions.named-arguments

命名参数允许根据参数名而不是参数位置向函数传参。这使得参数的含义自成体系,参数与顺序无关,并允许任意跳过默认值。 命名参数通过在参数名前加上冒号来传递。允许使用保留关键字作为参数名。

 1<?php
 2
 3function test(int $a, int $b = 2, int $c = 3): void
 4{
 5    echo $a . $b . $c;
 6}
 7
 8test(1); // 123
 9test(1, c: 4); // 124
10test(c: 4, b: 5, a: 1); // 154

注解

引自 https://www.php.net/manual/zh/language.attributes.php

注解功能提供了代码中的声明部分都可以添加结构化、机器可读的元数据的能力, 注解的目标可以是类、方法、函数、参数、属性、类常量。 通过 反射 API 可在运行时获取注解所定义的元数据。 因此注解可以成为直接嵌入代码的配置式语言。

构造器属性提升

引自 https://www.php.net/manual/zh/language.attributes.php

构造器的参数可以直接提升为类的成员属性,示例如下:

 1class User
 2{
 3    public function __construct(public string $username = '张三')
 4    {
 5
 6    }
 7}
 8
 9echo (new User)->username; // 张三
10echo (new User('李四'))->username; // 李四

联合类型

引自 https://www.php.net/manual/zh/language.types.declarations.php#language.types.declarations.composite.union

联合类型接受多个不同的简单类型做为参数。声明联合类型的语法为 T1|T2

 1<?php
 2
 3declare (strict_types = 1);
 4
 5function test(int|float $a): void
 6{
 7    echo $a;
 8}
 9
10test(1); // 1
11test(1.5); // 1.5
12test('1.5'); // 严格模式下报错

match表达式

引自 https://www.php.net/manual/zh/control-structures.match.php#control-structures.match

有点类似switch,具体参考文档

Nullsafe 运算符

引自 https://www.php.net/manual/zh/language.oop5.basic.php#language.oop5.basic.nullsafe

类属性和方法可以通过 “nullsafe” 操作符访问: ?->。 除了一处不同,nullsafe 操作符和以上原来的属性、方法访问是一致的: 对象引用解析(dereference)为 null 时不抛出异常,而是返回 null。 并且如果是链式调用中的一部分,剩余链条会直接跳过。

此操作的结果,类似于在每次访问前使用 is_null() 函数判断方法和属性是否存在,但更加简洁。

 1<?php
 2
 3// 自 PHP 8.0.0 起可用
 4$result = $repository?->getUser(5)?->name;
 5
 6// 上边那行代码等价于以下代码
 7if (is_null($repository)) {
 8    $result = null;
 9} else {
10    $user = $repository->getUser(5);
11    if (is_null($user)) {
12        $result = null;
13    } else {
14        $result = $user->name;
15    }
16}

PHP8.0.x => PHP8.1.x

完整更新记录 https://www.php.net/manual/zh/migration81.php

readonly关键字

引自 https://www.php.net/manual/zh/language.oop5.properties.php#language.oop5.properties.readonly-properties

readonly修饰符用于类成员属性,可以将成员属性标记为只读。

 1<?php
 2
 3class User
 4{
 5    public readonly string $username = '张三';
 6}
 7
 8$user = new User;
 9echo $user->username; // 张三
10$user->username = '李四'; // 报错

枚举类

引自 https://www.php.net/manual/zh/language.enumerations.php

1<?php
2enum HttpCode: int
3{
4    case Success = 200;
5}
6
7echo HttpCode::Success->value; // 200

纯交集类型

当一个值需要同时满足多个类型约束时,使用交集类型。注意,目前无法将交集和联合类型混合在一起,例如 A&B|C。

1<?php
2function generateSlug(HasTitle&HasId $post)
3{
4    return strtolower($post->getTitle()) . $post->getId();
5}

新的初始化器

对象现在可以用作默认参数值、静态变量和全局常量,以及属性参数。

 1<?php
 2
 3class Bar
 4{
 5    public function __construct(
 6        public string $name = ''
 7    ) {}
 8}
 9class Foo
10{
11    public function __construct(
12       public Bar $bar = new Bar('123')
13    ) {
14
15    }
16}
17echo (new Foo)->bar::class; // Bar
18echo (new Foo)->bar->name; // 123

Fibers

待补充