PHP 8.1 新特性
PHP 8.1已经在 2021 年 11 月 25 日发布,在本文中,我们将逐一介绍所有功能、性能改进、更改和弃用。
- 纯交集类型。
- Enum 枚举。
- Never 类型。
- Fibers(虚拟线程)。
- readonly 只读属性。
- final 常量。定义最终类常量。
- 新的 fsync()和 fdatasync()函数。
- 新的 array_is_list()函数。
- 新的 Sodium XChaCha20 函数。
- 新的 IntlDatePatternGenerator 类。
- 支持 AVIF 图像格式。
- 新的$_FILES:目录上传的 full_path 键。
- 对字符串键控数组的数组解包支持。
- 显式八进制数字表示法。
- MurmurHash3 和 xxHash 哈希算法支持。
- DNS-over-HTTPS(DoH)支持。
- 使用 CURLStringFile 从字符串上传文件。
- 新的 MYSQLI_REFRESH_REPLICA 常量。
- 使用继承缓存提高性能。
- 一流的可调用语法。
纯交集类型
PHP 8.1 中新增交集类型。交集类型要求输入是所有指定类型。当您使用大量接口时,交集类型特别有用:
function generateSlug(HasTitle&HasId $post) { return strtolower($post->getTitle()) . $post->getId(); }
如果你喜欢这种编程风格,你需要创建一个新的接口 Sluggable 并在$post 中实现它,交集类型摆脱了这种开销。
never 类型
PHP 8.1 中新增 never 类型,可以用来表示整个程序在一个函数中终止了。可以通过抛出异常、调用 exit、或是其他类似的函数来实现。
function dd(mixed $input): never { // 输出内容 exit; }
never
与void
的不同之处是,void
表示程序还在继续运行。这似乎是一个新奇的功能,但实际上对于静态分析来说却是一个非常有用的功能。
枚举 Enum
现在我可以把那些永远不会改变的数据存到枚举中,而不用再存入到一张永远不会修改的表格中。
// PHP 8 之前 class Method { public const GET = 'GET'; public const POST = 'POST'; public const PUT = 'PUT'; public const PATCH = 'PATCH'; public const DELETE = 'DELETE'; } // PHP 8 enum Method: string { case GET = 'GET'; case POST = 'POST'; case PUT = 'PUT'; case PATCH = 'PATCH'; case DELETE = 'DELETE'; }
// PHP 8 之前 trait SendsRequests { public function send(string $method, string $uri, array $options = []): Response { if (! in_array($method, ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'])) { throw new InvalidArgumentException( message: "Method [$method] is not supported.", ); } return $this->buildRequest()->send( method: $method, uri: $uri, options: $options, ); } } // PHP 8 trait SendsRequests { public function send(Method $method, string $uri, array $options = []): Response { return $this->buildRequest()->send( method: $method->value, uri: $uri, options: $options, ); } }
它让我的方法可以通过类型透视准确把握传入的参数,并减少由于不支持类型抛出异常的可能性。如果我们想要扩展支持,我们只需要在 Enum 中添加新的case
。
array_is_list
严格检查数组中的键是否按照数字顺序排列。从 0 开始排序且不能中断。
$arr = ['a', 'b', 'c']; var_dump(array_is_list($arr)); // true $arr = [1=>'a', 'b', 'c']; var_dump(array_is_list($arr)); // false $arr = [0=>'a', 'b', 'c']; var_dump(array_is_list($arr)); // true $arr = [0=>'a', 2=>'b', 'c']; var_dump(array_is_list($arr)); // false
数组解包
PHP 7.4 添加了对使用数组展开运算符(…
)进行数组解包的支持。它可以作为使用array_merge()
函数的更快替代方法。但是,此特性仅限于数字键数组,因为在合并具有重复键的数组时,解包字符串键数组会导致冲突。
但是,PHP 8.1 添加了对命名参数的支持,消除了这个限制。因此,数组解包现在也支持使用相同语法的字符串键数组:
$array = [...$array1, ...$array2];
// PHP 8.1 之前 $arrayA = ['a' => 1]; $arrayB = ['b' => 2]; $result = array_merge(['a' => 0], $arrayA, $arrayB); // ['a' => 1, 'b' => 2] // PHP 8.1 $arr1 = [1,2, 'a'=>['name'=>'lucy']]; $arr2 = [3,4, 'a'=>['age'=>19]]; $arr = ['a'=>0, ...$arr1, ...$arr2]; var_dump($arr); var_dump(array_merge($arr1, $arr2));
以前我们通常是进行复制或者合并数组。现在我们可以使用这一新特性,就可以实现数组的解包。
// PHP 8.1 之前 final class CreateNewClient implements CreateNewClientContract { public function handle(DataObjectContract $client, int $account): Model|Client { return Client::query()->create( attributes: array_merge( $client->toArray(), [ 'account_id' => $account, ], ), ); } } // PHP 8.1 final class CreateNewClient implements CreateNewClientContract { public function handle(DataObjectContract $client, int $account): Model|Client { return Client::query()->create( attributes: [ ...$client->toArray(), 'account_id' => $account, ], ); } }
最好的可调用语法
你现在可以通过调用一个方法,并向它传递...
,从而创建一个闭包。
function foo(int $a, int $b) { /* … */ } $foo = foo(...); $foo(a: 1, b: 2);
只读属性
以前我需要将想要用public
公开的属性改成protected
或者private
,这就是说我不得不因此为这个类中再添加getter
。
// PHP 8.1 之前 class Post { public function __construct() { protected string $title, protected string $content, } public function getTitle(): string { return $this->title; } public function getContent(): string { return $this->content; } } // PHP 8.1 class Post { public function __construct() {public readonly string $title,public readonly string $content, } }
使用 new 进行初始化设定
此 RFC 允许您在函数定义中使用关键字new
作为默认参数,也可以在属性参数等地方使用。
class MyController { public function __construct( private Logger $logger = new NullLogger(), ) {} }
您可以在这篇专题文章中阅读关于此功能的所有内容。
// PHP 8.1 之前 class BuyerWorkflow { public function __construct( private null|WorkflowStepContract $step = null ) { $this->step = new InitialBuyerStep(); } } // PHP 8.1 class BuyerWorkflow { public function__construct ( private WorkflowStepContract $step = new InitialBuyerStep(), ) {} }
在我看来,这一特性至少在代码上更加干净。在构造器上使用这一特性,我们不用再去担心会不会有传入null
值的可能问题–让类自己去处理这个问题。
静态数据一致性
// PHP 8.1 之前 class Num { public static function incr() { static $num = 0; $num++; var_dump($num); } } class Test extends Num { } Num::incr(); // 1 Num::incr(); // 2 Test::incr(); // 1 Test::incr(); // 2 // PHP 8.1 class Num { public static function incr() { static $num = 0; $num++; var_dump($num); } } class Test extends Num { } Num::incr(); // 1 Num::incr(); // 2 Test::incr(); // 3 Test::incr(); // 4
最终类常量
PHP 中的类常量可以在继承过程中被覆盖:
class Foo { public const X = "foo"; } class Bar extends Foo { public const X = "bar"; }
从 PHP 8.1 开始,您可以将这样的常量标记为final
,以防止出现这种情况:
class Foo { final public const X = "foo"; } class Bar extends Foo { public const X = "bar"; Fatal error: Bar::X cannot override final constant Foo::X }
同步函数
PHP 8.1 增加了fsync()
函数和fdatync()
函数,强制将文件同步更改到磁盘,并确保操作系统写缓冲区在返回前已刷新。
$file = fopen("sample.txt", "w"); fwrite($file, "一些内容"); if (fsync($file)) { echo "文件已成功保存到磁盘"; } fclose($file);
因为磁盘同步是一个文件系统的操作,所以fsync()
函数将只对普通文件流起作用。尝试同步非文件流将发出警告。
虚拟线程
从历史上看,PHP 代码几乎一直是同步代码。代码执行暂停,直到返回结果,即使是 I/O 操作。您可以想象为什么这个过程可能会使代码执行速度变慢。有多种第三方解决方案可以克服这一障碍,允许开发人员异步编写 PHP 代码,尤其是并发 I/O 操作。一些流行的示例包括amphp、ReactPHP和Guzzle。但是,在 PHP 中没有处理此类实例的标准方法。此外,在同一个调用堆栈中处理同步和异步代码会导致其他问题。
Fibers是 PHP 通过虚拟线程(或绿色线程)处理并行性的方式。它试图通过允许 PHP 函数中断而不影响整个调用堆栈来消除同步和异步代码之间的差异。
以下是 RFC 的承诺:
- 向 PHP 添加对 Fibers 的支持。
- 引入一个新的 Fiber 类和对应的反射类 ReflectionFiber。
- 添加异常类 FiberError 和 FiberExit 来表示错误。
- Fibers 允许现有接口(PSR-7、Doctrine ORM等)的透明非阻塞 I/O 实现。那是因为占位符(promise)对象被消除了。相反,函数可以声明 I/O 结果类型,而不是无法指定解析类型的占位符对象,因为 PHP 不支持泛型。
您可以使用 Fibers 开发全栈、可中断的 PHP 函数,然后您可以使用这些函数在 PHP 中实现协作多任务处理。当 Fiber 暂停整个执行堆栈时,您可以放心,因为它不会损害您的其余代码。
$fiber = new Fiber(function (): void { $value = Fiber::suspend('fiber'); echo "Value used to resume fiber: ", $value, "\n"; }); $value = $fiber->start(); echo "Value from fiber suspending: ", $value, "\n"; $fiber->resume('test');
你在上面的代码中创建了一个“fiber”,并立即用字符串挂起它 fiber。该 echo 声明用作 fiber 恢复的视觉提示。您可以通过调用$fiber->start()检索此字符串值。然后,使用字符串“test”恢复 fiber,该字符串是对 Fiber::suspend()的调用返回的。
完整代码执行会产生如下输出:
Value from fiber suspending: fiber Value used to resume fiber: test
这是 PHP Fibers 工作的准系统教科书示例。这是执行七个异步 GET 请求的另一个 Fibers 示例。
尽管如此,大多数 PHP 开发人员永远不会直接处理 Fibers。RFC 甚至提出了同样的建议:
Fibers 是大多数用户不会直接使用的高级功能。此功能主要针对库和框架作者,以提供事件循环和异步编程 API。Fibers 允许在任何时候将异步代码执行无缝集成到同步代码中,而无需修改应用程序调用堆栈或添加样板代码。
Fiber API 不应直接在应用程序级代码中使用。Fibers 提供了一个基本的、低级别的流控制 API 来创建更高级别的抽象,然后在应用程序代码中使用这些抽象。
Fibers ——又叫“虚拟线程”—是管理并行性的低级机制。您可能不会直接在您的应用程序中使用它,但像 Amphp 和 ReactPHP 等框架将大量使用它们。
$fiber = new Fiber(function (): void { $valueAfterResuming = Fiber::suspend('after suspending'); // … }); $valueAfterSuspending = $fiber->start(); $fiber->resume('after resuming');
如果您想读取更多关于fibers的信息。
显式八进制整数文字表示法
您现在可以使用0o
和0O
来表示八进制数。前面用0
当做前缀的表示法仍然有效。
016 === 0o16; // true 016 === 0O16; // true
内部方法返回类型
在升级到 PHP 8.1 时,你可能会见到此弃用通知:
Return type should either be compatible with IteratorAggregate::getIterator(): Traversable, or the #[ReturnTypeWillChange] attribute should be used to temporarily suppress the notice
你可能注意到了,这个错误信息会在使用phpunit/phpunit
、symfony/finder
和其他流行的开源包中出现。实际是因为内部函数开始使用正确的返回类型。如果想要从标准库(如 IteratorAggregate)中继承一个类,这时还需要添加返回类型。
修复方法很简单:如果第三方软件包中出现错误,请更新软件包的代码(其中大多数已在最新版本中修复)。如果代码中出现错误,您可以添加ReturnTypeWillChange
属性,在 PHP 9.0 之前抑制这些错误。下面是一个类扩展 DateTime 的示例:
class MyDateTime extends DateTime { /** * @return DateTime|false */ #[ReturnTypeWillChange] public function modify(string $modifier) { return false; } } // 或者你可以添加返回类型: class MyDateTime extends DateTime { public function modify(string $modifier): DateTime|false { return false; } }
限制$GLOBALS 的使用
一个对于$GLOBALS
使用方式的小更改将对所有阵列操作的性能产生重大影响。Nikita 在 RFC 中很好地解释了问题和解决方案。这个变动意味着$GLOBALS
不会在一些边缘情况下做出什么事情了。$GLOBALS
不再支持整体写入。下面的做法都将报错。
$GLOBALS = []; $GLOBALS += []; $GLOBALS =& $x; $x =& $GLOBALS; unset($GLOBALS);
引用调用$GLOBALS
会导致一个运行时错误
by_ref($GLOBALS);
Nikita 分析了 Packagist 上排名前 2000 的包,只发现了 23 个会受此变化影响的案例。我们可以得出结论,这种技术突破性变化的影响将是很小的,这也是为什么内部决定将它们添加到 PHP 8.1。请记住,大多数开发者都将从中获益,对我们代码产生的积极影响将无处不在。
资源到对象的迁移
这些变化是将所有资源转换为专用对象的长期愿景的一部分。你可以在这里了解更多。
- 带有 finfo 对象的 Fileinfo 函数:finfo_file、finfo_open 等函数用于接受和返回资源。从 PHP 8.1 开始,它们可以使用所有的 finfo 对象。
- 具有 IMAPConnection 对象的 IMAP 函数:就像 fileinfo 的变更一样,像 imap_body 和 imap_open 的 IMAP 函数不再使用资源的方式。
不推荐在内部函数中将 null 传递给不可为 null 的参数
这个更改很简单:内部函数当前可以为非 null 参数接受 null,这个 RFC 反对这种行为。例如,现在这样是可以的:
str_contains("string", null);
在 PHP 8.1中,这样的错误将抛出一个弃用警告,在 PHP 9 中,它们将变为类型错误。
从 false 自动变为非 false (Autovivification)
Autovivification 是 perl 中自造的词,但是在很多语言中都有应用。参考 Perl 的 autovivification 特性、维基百科 Autovivification(译者注)
PHP 天生支持 autovivification(自动从假的值中创建数组)。这个特性十分有用,特别是当值没有定义的时候。并且已经用在了很多 PHP 项目中。然而,有一点很奇怪,它居然允许从一个 false 和 null 创建一个数组。
您可以在 RFC 页面上阅读详细信息。总之,不推荐使用这种行为:
$array = false; $array[] = 2; Automatic conversion of false to array is deprecated
性能提升
Dmitry Stogov 对 opcache 进行了一些改进,他称之为「继承缓存(inheritance cache)」。这个新特性允许在缓存中连接两个类,就像 PHP 7.4 中两个类的预加载一样。
得益于这个新特性,Dmitry 说会提升 5%到 8%的性能,这是 PHP 8.1 中一个很好的小细节。
其他小变化
对于每一个发布版本,语言上都会有一些非常小的变化。所有这些都被列在 GitHub 的升级指南和弃用的 RFC中,如果你想知道每一个小细节,一定要去看看。
以下是一些最重要的变化:
- MYSQLI_STMT_ATTR_UPDATE_MAX_LENGTH 最大长度不再有效果。
- MYSQLI_STORE_RESULT_COPY_DATA 不再有效。
- PDO::ATTR_STRINGIFY_FETCHES 现在也可以用布尔值了。
- 当使用模拟准备语句时,PDO MySQL 和 Sqlite 结果集中的整数和浮点数将使用本地 PHP 类型而不是字符串返回。
- 像 htmlspecialchars 和 htmlentities 这样的函数在默认情况下也会转换为
'
;格式不正确的 utf-8 也会被 Unicode字符替换,而不是产生一个空字符串。 - hash、 hash_file 和 hash_init 将会加入一个额外的参数
$options
,它的默认值为[]
,不会影响已有代码。 - 新支持 MurmurHash3 和 xxHash。