• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • 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;
    }
    

    nevervoid的不同之处是,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的信息。


    显式八进制整数文字表示法

    您现在可以使用0o0O来表示八进制数。前面用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/phpunitsymfony/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。