【PHP8.1】PHP8.1の新機能

[PHP8.2](https://qiita.com/rana_kualu/items/fc4b02e2daaf102aa92f) / [PHP8.1](https://qiita.com/rana_kualu/items/a6601b49e0591eb42200) / [PHP8.0](https://qiita.com/rana_kualu/items/fe7998fbe773544d5d25) / [PHP7.4](https://qiita.com/rana_kualu/items/50f9f5735321fe995ff5)


**2021/11/26に[リリースされました](https://qiita.com/rana_kualu/items/e350b8ff80f929626eb9)**

2021/07/20、PHP8.1が[フィーチャーフリーズ](https://wiki.php.net/todo/php81)しました。
言語機能に関わるような機能の追加・変更が締め切られたということです。
今後はデバッグを繰り返しながら完成度を高めていき、2021/11/25にPHP8.1.0がリリースされる予定です。

というわけで[PHP8.1で実装されるRFC](https://wiki.php.net/rfc#php_81)を見てみましょう。


# RFC


## [Fibers](https://wiki.php.net/rfc/fibers)

賛成50反対14で受理。

[Fiber](https://qiita.com/rana_kualu/items/6247441f6fdd63c10a06)です。

PHPで非同期コードを書けるようになります。

```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でも書けるようになりそうですね。


## [Pure intersection types](https://wiki.php.net/rfc/pure-intersection-types)

賛成30反対3で受理。

[交差型](https://qiita.com/rana_kualu/items/f49582bc94e0bae23d9c)です。

PHP8.0で追加された[UNION型](https://qiita.com/rana_kualu/items/f75e254553157d8eb0b7)に引き続き、交差型も使用可能になります。

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

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

これで好きなだけ型パズルが作れるようになりますね。


## [Enumerations](https://wiki.php.net/rfc/enumerations)

賛成44反対7で受理。

[列挙型](https://qiita.com/rana_kualu/items/bdfa6c844125c1d0f4d4)です。

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

$val = Suit::Diamonds;
```

まずはENUMが実装されましたが、元々のRFCは最終的に[代数的データ型](https://wiki.php.net/rfc/adts)の実装を目指す巨大な計画です。

ENUMはクラスを下敷きに作られているため、個別にメソッドを実装したりとかなり複雑な作り込みもできるようになっています。

個人的にはENUMがなくてどうにもならないという事態に遭遇したことがないのでどのくらい有用かはよくわかりませんが、古来よりENUMをPHPに実装する試みが何度も行われているくらいなので、きっと需要も大きいのでしょう。


## [noreturn type](https://wiki.php.net/rfc/noreturn_type)

賛成42反対11で受理。

[never型](https://qiita.com/rana_kualu/items/134e65951c05de014165)です。

```php
function foo():never{
    exit;
}
```

途中でexitしたり常に例外を出すような、呼び出し元に返らない関数において返り値として書ける型`never`が追加されます。


## [Readonly properties 2.0](https://wiki.php.net/rfc/readonly_properties_v2)

[readonlyアクセス修飾子](https://qiita.com/rana_kualu/items/ae96400a0a6981eca64a)です。

```php
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`は、今ではここまで省略して書くことが可能です。

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


## [Deprecate implicit non-integer-compatible float to int conversions](https://wiki.php.net/rfc/implicit-float-int-deprecate)

賛成29反対0で受理。

`float`から`int`への暗黙の型変換にE_DEPRECATEDが発生するようになります。

`int`から`float`、`float`から`string`といった変換は、基本的に情報の損失が発生しません。
しかし`float`から`int`への変換はそうではありません。

```php
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になるので、strictな書き方をしている限りは何の影響もありません。


## [Final class constants](https://wiki.php.net/rfc/final_class_const)

賛成29反対4で受理。

[finalクラス定数](https://qiita.com/rana_kualu/items/db6e93beca61ce9eeea7)です。

```php
class FOO{
    final public const HOGE = 1;
}
```

継承したクラスで上書き変更できないことが保証される定数を書けるようになります。

これによって、クラス、メソッド、定数にfinalを書けるようになりました。
プロパティには書けませんが、上記のreadonlyがちょうど代替になるでしょう。


## [New in initializers](https://wiki.php.net/rfc/new_in_initializers)

賛成43反対2で受理。

[引数デフォルト値でnew](https://qiita.com/rana_kualu/items/e44235c21539d0a411b0)できるようになります。

```php
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で受けてからメソッド内で分岐するという不自然な書き方をしないといけなかったのですが、これが自然に書けるようになります。


## [Array unpacking with string keys](https://wiki.php.net/rfc/array_unpacking_string_keys)

賛成50反対0で受理。

配列アンパックの文字列キー対応です。

PHP7.4で[アンパックによる配列のマージ](https://wiki.php.net/rfc/spread_operator_for_array)が実装されました。

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

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

[array_merge](https://www.php.net/manual/ja/function.array-merge.php)や[配列結合](https://www.php.net/manual/ja/language.operators.array.php)とは違い、単純に全要素を順にくっつけるだけというもので、キーも0から振りなおされます。

どうせキーは振りなおされるのに、何故か文字列キーには対応していませんでした。

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

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

これを改善し、文字列キーでも配列アンパックできるようにします。

ただ何故か文字列キーについてはキーが振りなおされず、同じキーは後勝ちの上書きになるようです。

```php:PHP8.1
$array1 = ['a'=>1, 2];
$array2 = ['a'=>3, 4];

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

`[1, 2, 3, 4]`ではないみたいです。
よくわかりませんね。

## [Deprecate passing null to non-nullable arguments of internal functions](https://wiki.php.net/rfc/deprecate_null_to_scalar_internal_arg)

賛成46反対0で受理。

nullを許容しない内部関数への引数nullをE_DEPRECATEDにします。

たとえば[strlen](https://www.php.net/manual/ja/function.strlen)のシグネチャは`strlen(string $string): int`であり、引数はnull許容型ではありません。
しかし、普通にnullを渡せるしエラーも出ません。

```php
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にします。



## [Add return type declarations for internal methods](https://wiki.php.net/rfc/internal_method_return_types)

賛成17反対7で受理。

内部クラスをextendsした際に、返り値の型宣言をチェックするようにします。

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

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

[DateTime::modify](https://www.php.net/manual/ja/datetime.modify.php)の返り値は`DateTime|false`ですが、extendsした際に全く無視して`int`とか付けても普通に動きますしエラーも出ません。
これを互換しない型についてはE_DEPRECATEDを出すようにします。
PHP9ではTypeErrorになります。

さきほどのRFC同様、組み込み関数は甘めの判定になっているところが多いです。
ユーザ定義クラスをextendsした場合は、このコードは当然TypeErrorになります。


## [First-class callable syntax](https://wiki.php.net/rfc/first_class_callable_syntax)

賛成44反対0で受理。

[Closure::fromCallable](https://www.php.net/manual/ja/closure.fromcallable.php)の新文法です。

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

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

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

`...`は省略記号とかではなく、このままの文字です。

まあ、ただの糖衣構文です。
これで何がうれしいのかというと、文字列で渡さないので静的解析が可能になるというところです。

しかしこの構文がいきなり出てきたら、アンパックや普通の関数呼び出しと見間違って混乱する自信がある。


## [Add IntlDatePatternGenerator](https://wiki.php.net/rfc/intldatetimepatterngenerator)

賛成10反対0で受理。

ローカライズされた日付フォーマッタを導入します。

```php
$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](https://www.php.net/manual/ja/book.intl.php)は[ICUのそれ](https://unicode-org.github.io/icu-docs/apidoc/dev/icu4c/classicu_1_1DateTimePatternGenerator.html)をラップしているだけなので、PHPだからどうとか言われても困りますけどね。
`getBestPattern`とかいう名前も[実は元のまま](https://unicode-org.github.io/icu-docs/apidoc/dev/icu4c/classicu_1_1DateTimePatternGenerator.html#a41a97e72ded1cd3f8257946cfe01c222)だったりします。

しかしintlの情報とか使用例とかって[全然出てこない](https://qiita.com/midheartty/items/590b0911c000a24abe34)ので、`ja_JP`だったら`2021/04/24`になるのか`2021年4月24日`になるのか、そこらへんもよくわからないな。



## [Add array_is_list(array $array): bool](https://wiki.php.net/rfc/is_list)

賛成41反対1で受理。

関数[array_is_list](https://qiita.com/rana_kualu/items/4363107370f508717851)を追加します。

キーが0始まりで抜けの無い配列である場合にのみ、trueになります。

```php
	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](https://www.php.net/manual/ja/class.splfixedarray.php)とか使えばいいですし、そもそもPHPでは純粋な配列と抜けのある配列を区別する意味が全くないので、ユーザランドでの出番は多くないと思います。


## [Explicit octal integer literal notation](https://wiki.php.net/rfc/explicit_octal_notation)

賛成33反対0で受理。

[8進数の基数表示](https://qiita.com/rana_kualu/items/2663f4b9bf8aefcc3955)です。

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

8進数だけ接頭辞がなくてわかりにくいですね。
そこで`0o`による8進数表記をサポートします。

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

既存の書き方については、当面は削除される予定はありません。


## [Restrict $GLOBALS usage](https://wiki.php.net/rfc/restrict_globals_usage)

賛成48反対0で受理。

`$GLOBALS`の内部処理を変更するRFCです。

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

値をコピーしただけのはずなのに、何故か元の値も変わってしまっています。

このように`$GLOBALS`には色々と特殊な仕様が存在しており、そのせいで内部構成が複雑になり、パフォーマンスの問題も発生しているそうです。
そこで、このような特殊な仕様をなるべく排除します。

通常の使い方での互換性は概ね維持されますが、リファレンスを駆使した変なコードは動かなくなるみたいです。

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

まあ、普通はそもそも`$GLOBALS`が目に入った時点で拒否反応が出ますよね。


## [Change Default mysqli Error Mode](https://wiki.php.net/rfc/mysqli_default_errmode)

賛成21反対9で受理。

mysqliのエラーモードのデフォルトが例外になります。

PHP8.0まで[デフォルト値はMYSQLI_REPORT_OFF](https://www.php.net/manual/ja/mysqli-driver.report-mode.php)であり、あえて設定変更しないかぎり黙って死ぬという困った仕様でした。
PHP8.1以降デフォルト値は`MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT`になります。

PHP8.0で[PDOのエラーモードデフォルトも例外](https://wiki.php.net/rfc/pdo_default_errmode)になったので、それに合わせたものとなります。


## [Add fetch_column method to mysqli](https://wiki.php.net/rfc/mysqli_fetch_column)

賛成18反対2で受理。

`mysqli_result::fetch_column`メソッドです。

[mysqli](https://www.php.net/manual/ja/class.mysqli-result.php)にはfetch関数が色々あるのに、何故かfetch_columnがなかったので追加されます。

ただ個人的にはPDOでも[fetchColumn](https://www.php.net/manual/ja/pdostatement.fetchcolumn.php)を使ったことないし、ひとつのカラムだけ取り出すという需要がよくわかりません。


## [Mysqli bind in execute](https://wiki.php.net/rfc/mysqli_bind_in_execute)

賛成32反対0で受理。

[mysqli::execute](https://www.php.net/manual/ja/mysqli-stmt.execute.php)が実行時引数渡しに対応します。

```php
// 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以降は、PDO同様実行時に渡せるようになります。

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

個人的には完全にPDOしか使わないので、mysqliにいまさら色々追加されてもなあって感じが否めない。


## [fsync() Function](https://wiki.php.net/rfc/fsync_function)

賛成30反対1で受理。

関数`fsync`を追加します。

ソースを見てみると単にC言語の[fsync](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/fsync.2.html)のラッパーでした。

よくわからないので[参考URL](https://racchai.hatenablog.com/entry/2016/05/02/070000)とかを読んでみるに、[fwrite](https://www.php.net/manual/ja/function.fwrite.php)などの書き込み関数は、実は呼んだ時点でディスクに書き込み完了しているわけではなく、どこかにキャッシュされているだけなので、直後にプログラムが落ちたら書き込まれずに消滅してしまう危険性がある。
そこでキャッシュを出力する関数として`fflush`や`fsync`や`fdatasync`が存在し、`fsync`であれば実際にディスクに書き込み完了したところまで確認する。
ということのようです。

PHPには[fflush](https://www.php.net/manual/ja/function.fflush.php)は存在しましたが、これまでは無かった`fsync`を追加します。
あとRFCにははっきり書かれていませんが、[プルリク](https://github.com/php/php-src/pull/6650/files)にはしれっと`fdatasync`も入ってました。


## [Phasing out Serializable](https://wiki.php.net/rfc/phase_out_serializable)

賛成36反対0で受理。

[Serializable](https://www.php.net/manual/ja/class.serializable.php)をE_DEPRECATEDにします。


```php
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`](https://www.php.net/manual/ja/language.oop5.magic.php#object.serialize)が[実装](https://qiita.com/rana_kualu/items/993fff9662726a004660#new-custom-object-serialization-mechanism)され、`Serializable`より優先されるようになりました。
上記コードはPHP7.4以降`__serialize/__unserialize`だけが実行され、`serialize/unserialize`は無視されます。

今回E_DEPRECATEDになる条件は、`Serializable`だけが実装されていて、`__serialize/__unserialize`が実装されていない場合です。
両方とも実装されている場合は、PHP8.1でも警告は出ないようです。
両方とも実装されていない場合は、もちろん今までどおり何も出ません。


## [Static variables in inherited methods](https://wiki.php.net/rfc/static_variable_inheritance)

賛成38反対0で受理。

[静的変数を継承したときの挙動が微妙に変更](https://qiita.com/rana_kualu/items/e65844540581952e73ff)になります。

```php
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)
```

静的変数は、同じクラスであれば同じ変数、異なるクラスであれば別の変数になる、というよくわからない状態でした。
これを継承に関わらず常に同じ変数を見るようにします。

変更後は、クラス変数と同じ挙動になります。

```php
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)
```

静的変数とクラス変数でなぜか動作が微妙に異なっていたので、この動作を合わせるためのものです。
そもそもなんで異なっていたんだ。



## [Make reflection setAccessible() no-op](https://wiki.php.net/rfc/make-reflection-setaccessible-no-op)

賛成31反対0で受理。

[ReflectionProperty](https://www.php.net/manual/ja/reflectionproperty.setaccessible.php)および[ReflectionMethod](https://www.php.net/manual/ja/reflectionmethod.setaccessible.php)において、デフォルトで`setAccessible(true)`になります。

```php
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`は実質的に何もしなくなります。

まあ便利といえば便利だけど、もっと評価が分かれてもよさそうな提案なのに、全員賛成ってのは少々びっくり。


## [Deprecate autovivification on false](https://wiki.php.net/rfc/autovivification_false)

賛成34反対2で受理。

`false`から配列の自動生成を禁止します。

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

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

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

PHPでは、このように何もないところにいきなり配列値を突っ込むと、自動的に配列ができあがります。

このうち、`false`だけはPHP8.1から自動生成を禁止します。

これだけだと何で`false`だけ？って思いますが、実は`''`や`0`や`true`からの配列自動生成は既に前から禁止されているので、そちらのスカラー型の動作に合わせたということです。
むしろ`undefined`と`null`が例外です。

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


## [Deprecations for PHP 8.1](https://wiki.php.net/rfc/deprecations_php_8_1)

様々な古い機能や書き方について、PHP8.1でDepreatedにし、PHP9で削除するという提案。

[詳細は別記事で解説します](https://qiita.com/rana_kualu/items/c894b2160d1f8799eed2)が、ほとんどは聞いたこともない関数や元より動いていなかった設定などです。
たとえば[date_sunrise](https://www.php.net/manual/ja/function.date-sunrise.php)と[date_sunset](https://www.php.net/manual/ja/function.date-sunset.php)を削除して[date_sun_info](https://www.php.net/manual/ja/function.date-sun-info.php)に集約します。

個人的に気になったのは[strftime](https://www.php.net/manual/ja/function.strftime)くらいですかね。
ほとんどのユーザにとっては影響はないと思われます。


# 感想

[PHP8.0](https://qiita.com/rana_kualu/items/fe7998fbe773544d5d25)や[PHP7.4](https://qiita.com/rana_kualu/items/50f9f5735321fe995ff5)に比べるとそこまで多いというわけではありませんが、それでも様々な機能追加や改善がなされています。
今回の目玉はFiberと型関連でしょうか。

Fiberは『上から順に実行する』というPHPの原則を破る、大きな可能性を秘めた存在です。
今後のライブラリの対応次第では、有力な機能となることでしょう。

また型については、交差型・never型・列挙型の追加、継承時の挙動厳格化などが含まれています。
PHPは7以降、型関連の実装・機能追加が相次いで行われてきました。
今ではもはや、下手な静的型付け言語よりずっと厳格な型運用を行うことすら可能になっています。

あとArray unpacking with string keysは、これまでと書式ががらっと変わるのでPHP8.1以前と両用していると導入は難しそうですが、使い慣れると非常に便利になりそうです。

ということで、年末にリリース予定のPHP8.1をみんなも是非やりましょう。