【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版本中尝试一下。