【PHP8.1】PHP8.1的新功能

PHP8.2 / PHP8.1 / PHP8.0 / PHP7.4 可选择的 PHP 版本。

2021年11月26日发行

2021年7月20日,PHP8.1 进入了特性冻结阶段。
这意味着与编程语言功能相关的添加和修改已经截止。
在接下来的日子里我们将不断进行调试,提高版本的完善度,
计划于2021年11月25日发布 PHP8.1.0 版本。

让我们来看一下即将在PHP8.1中实施的RFC。

RFC是“请求评论”,它是一种关于互联网标准、协议或方法的文件。

纤维

通过了,50人赞成,14人反对。

光纤。

你将能够在PHP中编写异步代码。

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('fiber');
    echo "レジュームした。$value: ", $value, "\n";
});

$value = $fiber->start();

echo "一時停止した。$value: ", $value, "\n";

$fiber->resume('test');

// 実行結果
一時停止した$value: fiber
レジュームした$value: test

在事件循环中,像 Promise 这样的处理,到目前为止在 PHP 中是困难的,但终于可以编写了。
基本上这不是为一般用户而设计的功能,所以需要等待库的支持,但 PHP 似乎也可以编写非阻塞的代码了。

纯交叉类型

以30票赞成、3票反对的结果通过了。

这是一个交叉型。

继PHP8.0新增了UNION类型之后,交叉类型也可供使用。

class A {
    // UNION型 $aはTraversableもしくはCountableである
    private Traversable|Countable $a;

    // 交差型 $bはTraversableかつCountableである
    private Traversable&Countable $b;
}

现在你可以自由地制作任意数量的拼图了,是吧。

列举

以44票赞成,7票反对通过。

这是一个列举类型。

enum Suit {
  case Hearts;
  case Diamonds;
  case Clubs;
  case Spades;
}

$val = Suit::Diamonds;

首先,ENUM已被实现,但最初的RFC旨在最终实现代数数据类型的庞大计划。

由于ENUM类是建立在类的基础上的,它可以独立地实现方法,因此可以进行相当复杂的构建。

我个人从未遇到过没有ENUM而无法解决的情况,所以对其有多大用处并不清楚,但是自古以来,已经多次尝试在PHP中实现ENUM,可见需求肯定很大。

无返回类型

42票赞成,11票反对通过。

永远的。

function foo():never{
    exit;
}

在某些会中途退出或始终抛出异常的、无法返回给调用者的函数中,将添加一种名为never的类型作为返回值。

只读属性 2.0

只读访问修饰符。

class Test {
    public readonly string $prop;

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

$test = new Test("foobar");

// 読み込みはOK
var_dump($test->prop); // string(6) "foobar"

// 書き込みはNG
$test->prop = "foobar"; // Error: Cannot modify readonly property Test::$prop

这是一个能够在一次初始化后保证不会再被改变的属性。使用protected或private的理由中有98%是因为希望其可被读取但不可被写入,而这个属性能够解决这种用例。

值得一提的是,现在可以将上述Test类省略成这样简洁的写法。

class Test {
    public function __construct(public readonly string $prop) {}
}

弃用隐式非整数兼容的浮点数转换为整数

以29票赞成、0票反对被接受。

从float到int的隐式类型转换会触发E_DEPRECATED警告。

基本上,從int到float或從float到string的轉換不會有信息損失。
然而,從float到int的轉換就不是這樣。

function toInt(int ...$arg){
    var_dump($arg);
}

function toFloat(float ...$arg){
    var_dump($arg);
}

function toString(string ...$arg){
    var_dump($arg);
}

toInt(1, '1', 1.5, '1.5');    // [1, 1, 1, 1]
toFloat(1, '1', 1.5, '1.5');  // [1, 1, 1.5, 1.5]
toString(1, '1', 1.5, '1.5'); // ['1', '1', '1.5', '1.5']

只有在转换为’int’时丢失了信息。因此,将来将发出有关这种隐式转换的警告。

对于明确的转换部分,例如(int)1.5或intval(1.5),可以像以前一样顺利地进行转换,没有问题。

然而,即使现在添加了declare(strict_types=1),这个转换也会导致TypeError,在严格的写法下也不会有任何影响。

最终类的常量

以29票赞成、4票反对的结果通过。

这是一个final类的常量。

class FOO{
    final public const HOGE = 1;
}

你将能够编写一个在继承的类中保证无法被重新更改的常量。

通过这种方式,我们现在可以在类、方法和常量上使用final关键字。
不能在属性上使用,但是上述的readonly可能会成为一个很好的替代品。

初始化器中的新功能

以43票赞成和2票反对的结果通过接受。

可以通过参数的默认值来进行实例化。

class Test {
	// PHP8.0まで
    private Logger $logger;
    public function __construct(
        ?Logger $logger = null,
    ){
        $this->logger = $logger ?? new NullLogger;
    }

	// PHP8.1以降
    public function __construct(
        private Logger $logger = new NullLogger,
    ){}
}

以前,参数的默认值只能是标量值,因此我们不得不使用一种不太自然的方法,即在方法内部接收null并进行分支判断。但现在,可以自然地编写这个功能。

使用字符串键进行数组解包

以50票赞成、0票反对通过。

这是关于字符串键对应的数组解包。

PHP7.4实现了通过解包来合并数组的功能。

$array1 = [10=>1, 2];
$array2 = [10=>3, 4];

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

与array_merge函数或数组合并不同的是,这只是简单地按顺序连接所有元素,同时键也会从0重新开始分配。

既然密钥会重新生成,却不知为何没有对字符密钥进行支持。

$array1 = ['a'=>1, 2];
$array2 = ['a'=>3, 4];

$array = [...$array1, ...$array2]; // Fatal Error: Cannot unpack array with string keys

我们将对此进行改进,以使其能够通过字符串键进行数组解包。

只有在字符键上,似乎不会重新给键排序,而是将相同的键进行覆盖。

$array1 = ['a'=>1, 2];
$array2 = ['a'=>3, 4];

$array = [...$array1, ...$array2]; // ['a'=>3, 2, 4]

好像不是 [1, 2, 3, 4]。
不太清楚呢。

弃用将空值传递给内部函数的非空参数

以46票赞成、0票反对通过。

将传递给不接受 null 值的内部函数的参数设置为 E_DEPRECATED。

举个例子,strlen的签名是strlen(string $string): int,参数不可为空。
然而,实际上可以正常传递null,并且不会出错。

strlen(null); // エラー出ない

function my_strlen(string $string): int{
    return strlen($string);
}
my_strlen(null); // TypeError Argument 1 passed to my_strlen() must be of the type string, null given

由于历史原因,内部函数往往对参数进行宽松的判断,而用户定义的函数将正确地产生TypeError。
为了解决这种矛盾,提出了这个RFC。

然而,如果突然引发 TypeError 会造成极大混乱,所以在 PHP8.1 中将其设为 E_DEPRECATED,而在 PHP9 或之后的版本中将用户自定义函数与 TypeError 设定为相同。

为内部方法添加返回类型声明

以17票赞成,7票反对的结果被接受。

在内部类继承时,将检查返回值的类型声明。

class MyDateTime extends DateTime
{
    public function modify(string $modifier):int
    {
        return 1;
    }
}

$dt = new MyDateTime();
$dt->modify('modifier');

DateTime::modify的返回值是DateTime|false,但是即使在扩展时完全忽略并添加了int等类型,它仍然可以正常工作且不会出错。
对于不兼容的类型,将发出E_DEPRECATED警告。
在PHP9中,将导致TypeError。

与之前的RFC一样,内置函数的判断条件通常较为宽松。如果继承了用户定义的类,那么这段代码自然会产生TypeError错误。

一流的可调用语法

以44票支持、0票反对通过。

这是Closure::fromCallable的新语法。

// 同じ
$fn = Closure::fromCallable('strlen');
$fn = strlen(...);

// 同じ
$fn = Closure::fromCallable([$this, 'method']);
$fn = $this->method(...)

// 同じ
$fn = Closure::fromCallable([Foo::class, 'method']);
$fn = Foo::method(...);

… 不是省略符号之类的,而是直接的文字形式。

嗯,這只是一種表面的語言結構而已。
這樣做的好處是不需要傳輸字符串,因此可以進行靜態分析。

但是如果这个句子突然出现,我有信心会将其误认为解包或普通的函数调用而感到困惑。

添加IntlDatePatternGenerator

一致通过并接受,赞成10票,反对0票。

引入本地化的日期格式化程序。

$skeleton = "YYYYMMdd";

$today = \DateTimeImmutable::createFromFormat('Y-m-d', '2021-04-24');

$dtpg = new \IntlDatePatternGenerator("de_DE");
$pattern = $dtpg->getBestPattern($skeleton);
echo "de: ", \IntlDateFormatter::formatObject($today, $pattern, "de_DE"), "\n"; // de: 24.04.2021

$dtpg = new \IntlDatePatternGenerator("en_US");
$pattern = $dtpg->getBestPattern($skeleton), "\n";
echo "en: ", \IntlDateFormatter::formatObject($today, $pattern, "en_US"), "\n"; // en: 04/24/2021

这个…怎么样?

嗯,PHP的intl只是封装了ICU的功能,所以即使说是因为是PHP而产生问题,我也很困惑。而且,getBestPattern这个名称实际上也没有改动。

由于intl上几乎没有相关信息或使用示例,所以我对于是使用ja_JP来表示2021/04/24还是用2021年4月24日来表示不太清楚。

增加一个函数array_is_list(array $array),返回一个布尔值

以41票赞成、1票反对的结果通过。

添加一个名为array_is_list的函数。

只有当键是以0开头且没有遗漏的数组时,才会为true。

	array_is_list([1, 2]); // true
	array_is_list([1=>1, 2]); // false
	array_is_list([1, 2=>2]); // false
	array_is_list(['a'=>1, 2]); // false

使用PHP中被称为”数组”的实际上是有序哈希表,并不存在其他语言中所说的”数组”的概念。
通过使用此函数,您可以确定它是否与其他语言中所说的”数组”相同。

如果您想使用纯粹的数组,可以使用SplFixedArray之类的。而且,在PHP中,纯粹的数组和带空元素的数组没有区别,所以在用户界面中几乎没有什么用处。

明确的八进制整数字面量表示法

受到33人的支持和0人的反对,通过。

这是表示八进制数的基数。

$a = 16;         // 10進数
$a = 0x10;       // 16進数
$a = 0b00010000; // 2進数
$a = 020;        // 8進数

为了解决八进制数没有前缀导致的混淆问题,我们将支持使用0o表示八进制数。

$a = 0o20; // PHP8.1以降OK

关于现有的写作方式,暂时没有计划删除。

限制$GLOBALS的使用

以48票赞成、0票反对通过。

这是一个修改$GLOBALS内部处理的RFC。

$a = 1;
$globals = $GLOBALS; // 値をコピー
$globals['a'] = 2;
var_dump($a); // int(2)

尽管只是复制了值,但原始值不知何故也被改变了。

根据说法,全局变量中存在各种特殊规格,导致内部结构变得复杂,并且出现了性能问题。
因此,我们希望尽可能地消除这些特殊规格。

在通常情况下,通常的使用方法基本上是兼容的,但是依赖于参考资料的奇怪代码似乎无法运行。

foreach ($GLOBALS as $var => $_) $$var =& $GLOBALS[$var]; // 動かなくなる

嗯,通常情况下,在看到$GLOBALS这个变量时,大多数人会产生一种拒绝的反应吧。

更改默认的mysqli错误模式

以21票赞成,9票反对通过。

mysqli的错误模式默认为异常。

从PHP8.0开始,默认值为MYSQLI_REPORT_OFF,除非特意更改设置,否则它会在静默中死机,这是一个令人困扰的规范。从PHP8.1开始,默认值将更改为MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT。

由于在PHP8.0中,PDO的错误模式已更改为默认抛出异常,所以我们相应地做了修改。

给mysqli添加fetch_column方法。

18人赞成,2人反对,通过。

这是 mysqli_result::fetch_column 方法。

由于mysqli中缺少fetch_column函数,因此需要进行补充,尽管已存在许多其他fetch函数。

就我个人而言,我从未使用过PDO的fetchColumn方法,也不太理解只提取一个列的需求。

Mysqli绑定在执行中

以32票赞成、0票反对的结果通过了。

mysqli::execute方法可以传入运行时参数。

// PDO 事前渡し
$stmt = $pdo->prepare('INSERT INTO users(id, name) VALUES(?,?)');
$stmt->bindParam(0, $id);
$stmt->bindParam(1, $name);
$stmt->execute();

// PDO 実行時渡し
$stmt = $pdo->prepare('INSERT INTO users(id, name) VALUES(?,?)');
$stmt->execute([$id, $name]);

// mysqli 事前渡し
$stmt = $mysqli->prepare('INSERT INTO users(id, name) VALUES(?,?)');
$stmt->bind_param('ss', $id, $name);
$stmt->execute();

为什么mysqli在参数执行时无法提供支持。
从PHP8.1开始,mysqli将像PDO一样在执行时支持传参。

// mysqli 実行時渡し PHP8.1以降
$stmt = $mysqli->prepare('INSERT INTO users(id, name) VALUES(?,?)');
$stmt->execute([$id, $name]);

在个人看来,我只会完全使用PDO,所以对mysqli现在添加了各种功能也感觉有点多余。

fsync()函数

以30票赞成、1票反对的结果被接受。

添加fsync函数。

当我查看源代码时,我发现它只是一个C语言fsync函数的封装。

根据我阅读参考网址等内容,写入函数(如fwrite)实际上并不会在调用时立即完成磁盘写入,而只是被缓存起来了。因此,如果程序在写入后立即崩溃,就有可能导致数据未被写入而消失。

为了解决这个问题,存在着用于输出缓存的函数,如fflush、fsync和fdatasync。而使用fsync函数可以确保实际完成了磁盘写入。

大致就是这个意思。

PHP中原本就有fflush这个函数,但是最近新增了fsync。虽然RFC中没有明确提到,但在pull request中悄悄地加入了fdatasync。

逐渐淘汰Serializable

36人赞成,0人反对,被接受。

将Serializable设置为E_DEPRECATED。

class HOGE implements Serializable{
    // Serializable
    public function serialize() {
        return serialize($this->data);
    }
    public function unserialize($data) {
        $this->data = unserialize($data);
    }
    
    // マジックメソッド
    public function __serialize() {
        return serialize($this->data);
    }
    public function __unserialize($data) {
        $this->data = unserialize($data);
    }
}

PHP7.4新增了魔术方法__serialize/__unserialize,并且优先于Serializable接口。
以上代码在PHP7.4以上只会执行__serialize/__unserialize,serialize/unserialize会被忽略。

如果只有 Serializable 接口被实现但未实现 __serialize / __unserialize 方法,那么这次就会发出 E_DEPRECATED 警告。
如果两者都实现了,似乎在 PHP8.1 中不会发出警告。
如果两者都未实现,那当然就像以前一样,什么都不会发生。

继承方法中的静态变量

以38票赞成、0票反对的结果被接纳。

当继承静态变量时,其行为会微妙地变化。

class A {
    public static function counter() {
        static $i = 0;
        return ++$i;
    }
}
class B extends A {}

var_dump(A::counter()); // int(1)
var_dump(A::counter()); // int(2)
var_dump(B::counter()); // <PHP8.1:int(1)  >=PHP8.1:int(3)
var_dump(B::counter()); // <PHP8.1:int(2)  >=PHP8.1:int(4)

无论继承与否,静态变量将始终被视为相同的变量,即使在不同的类中也是如此,这样就消除了我不太理解的状态。

变更后,将具有与类变量相同的行为。

class A {
    static $i = 0;
    public static function counter() {
        return ++self::$i;
    }
}
class B extends A {}

var_dump(A::counter()); // int(1)
var_dump(A::counter()); // int(2)
var_dump(B::counter()); // int(3)
var_dump(B::counter()); // int(4)

由于静态变量和类变量的原因,它们的行为在微妙的方式上有所不同,所以这个是为了使它们的行为一致。
那么,它们为什么不同呢?

将反射的setAccessible()方法变为无操作。

以31票赞成、0票反对的结果被接受。

在ReflectionProperty和ReflectionMethod中,默认情况下setAccessible(true)。

class HOGE{
    private static int $foo = 1;
    private static function bar()
    {
        return 2;
    }
}

// ReflectionProperty
$p = new ReflectionProperty(HOGE::class, 'foo');
$p->setAccessible(true);
$p->getValue(); // 1

// ReflectionMethod
$p = new ReflectionMethod(HOGE::class, 'bar');
$p->setAccessible(true);
$p->invoke(); // 2

这个setAccessible,反正每次都得指定,干嘛不一开始默认就好呢,这是主张。
有了这个,默认写法就可以像(new ReflectionProperty(HOGE::class, ‘foo’))->getValue();这样简洁。
而setAccessible实际上就什么都不需要做了。

嗯,虽然可以说它方便,但更希望能有更有争议性的提议,让大家意见不一致,全体都赞同还是有点令人吃惊的。

取消对错误的自动创建键值的支持。

以34票赞成、2票反对的结果通过。

禁止从false开始自动生成数组。

// undefinedから
$arr[] = 'some value';
$arr['doesNotExist'][] = 2; // ['some value', 'doesNotExist'=>[2]]

// nullから
$arr = null;
$arr[] = 2; // [2]

// falseから
$arr = false;
$arr[] = 2; // [2]

在PHP中,如果在一个空的位置突然放入一个数组值,自动就会创建一个数组。

其中一项,从PHP8.1开始,将禁止自动生成功false。

为什么只有false呢?实际上,之所以只有false,是因为”、0和true生成数组的功能早在之前已经被禁止了,这是为了与其它标量类型的操作相一致。
相反,undefined和null是例外情况。

// trueから
$arr = true;
$arr[] = 2; // Warning: Cannot use a scalar value as an array

PHP 8.1所进行的弃用更新

对于各种旧功能和写法,提议在PHP8.1中标记为过时,然后在PHP9中删除。

详细内容将在另一篇文章中解释,但主要是关于一些从未听说过的函数和从一开始就没有运作的设置。
例如,将date_sunrise和date_sunset删除,将它们合并为date_sun_info。

在我個人看來,只有strftime會引起我的注意。
我認為對大多數用戶來說,這不會有太大的影響。

对此的想法
对这件事情的感受

与PHP8.0或PHP7.4相比,并不是非常丰富,但仍然有许多功能增加和改进。
本次亮点可能是Fiber和类型相关的改进。

Fiber作为一项具有巨大潜力的存在,违背了PHP上的“自上而下执行”的原则。根据未来的库适配情况,它有望成为强大的功能。

PHP 7以后,已经陆续进行了与类型相关的实现和功能增加,包括添加了交叉类型、never类型和枚举类型,并且严格化了在继承时的行为。
现在,我们甚至可以执行比任何手动静态类型语言更严格的类型操作。

此外,使用字符串键进行的数组解包在PHP8.1之前和之后的写法完全变化,因此该功能的引入可能较为困难,但一旦熟悉使用,将会非常方便。

因此,大家务必在年底发布的PHP8.1版本中尝试一下。

广告
将在 10 秒后关闭
bannerAds