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

[PHP8.4](https://qiita.com/rana_kualu/items/b195b1adc60b763843d9) / [PHP8.3](https://qiita.com/rana_kualu/items/91111443105a75a55e2f) / [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)


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

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

# RFC



## [Property access hooks](https://wiki.php.net/rfc/property-hooks)

賛成42反対2で受理。

[プロパティフック](https://qiita.com/rana_kualu/items/06d68549058966a34c2b)です。

```php
class HOGE{
	public string $tel{
		set{
			if(!ctype_digit($value)){
				 throw new ValueError("電話番号は数値のみ");
			}
			if(strlen($value) < 10){
				 throw new ValueError("電話番号は10文字以上");
			}
			$this->tel = $value;
		}
		get=> '電話番号は' . $this->tel;
	}
}

$hoge = new HOGE();

$hoge->tel = '123456789012'; // OK
$hoge->tel = 'abcdefghijkl'; // Uncaught ValueError: 電話番号は数値のみ
$hoge->tel = '123';          // Uncaught ValueError: 電話番号は10文字以上

echo $hoge->tel; // "電話番号は123456789012"
```

プロパティに直接setter/getterを書くことができます。
これによって無駄なsetter/getterメソッドをとりあえず書いておく手間がなくなり、プロパティへの後からの検証機能の追加なども容易になります。


## [Property hook improvements](https://wiki.php.net/rfc/hook_improvements)

賛成33反対0で受理。

プロパティフックのパフォーマンス改善と、それに伴うトレードオフです。

```php
class C
{
    public string $val = 'a' {
        get {
          return $this->val . $this->suffix();
        }
    }
 
    public function suffix(): string
    {
        return $this->val;
    }
}
 
$c = new C();
print $c->val;
```

このコードは`suffix()`内でvalを読んでいるので`val()`が呼ばれ、`val()`の中で`suffix()`が呼ばれているので再度valが読み込まれ、と無限ループになります。
プロパティフックの当初の実装では、このコードは専用のエラー検出を行っていました。

ところでプロパティフックのコード最適化をがんばっていたところ、劇的なパフォーマンス向上を成し遂げたかわりに上記コードが専用のエラーにならなくなりました。
かわりに無限ループでスタック上限にひっかかるという既存のPHPエラーになります。

そのトレードオフを押してでも導入する価値はあるだろうかというRFCですが、もちろん賛成多数で受理されました。

ちなみにこのRFCによって、プロパティフックのオーバーヘッドは『__get()と同じくらい』から『メソッドに比べると無きに等しい』まで改善されたそうです。
すごいですね。


## [Asymmetric Visibility v2](https://wiki.php.net/rfc/asymmetric-visibility-v2)

賛成24反対7で受理。

非対称可視性です。
[かつてPHP8.3向けに提出されたものの却下された](https://qiita.com/rana_kualu/items/403eec27f802402d0580)RFCをブラッシュアップしたものであり、今回無事に受理となりました。


```php
class Foo {
    public private(set) string $bar = 'baz';
}

$foo = new Foo();
var_dump($foo->bar); // "baz"
$foo->bar = 'beep';  // Visibility error
```

読み込みはpublicなのでどこからでも可能ですが、書き込みはprivateなので自分自身からしか書き込むことができません。
setter/getterを使う理由の98%は『見られてもいいけど書き込ませたくない』なので、今後のPHPはきっと`public private(set)`が定型文になっていくことでしょう。

なんとなく機能がプロパティフックと被っている気もしますが、プロパティフックは『setメソッドの動作を規定する』、可視性は『そもそもsetメソッドにアクセスできるかを規定する』であって、それぞれ独立した概念です。
両方同時に使うことももちろん可能です。

```php
class Everything {
  public protected(set) $answer {
    get => 42;
    set => print "Why?";
  }
}
```

書き込みは子クラスからのみ可能ですが実際は書き込まれず、読み込みはどこからでもできますが42が返ってきます。


## [#[\Deprecated] Attribute](https://wiki.php.net/rfc/deprecated_attribute)

賛成23反対6で受理。

みんな大好き非推奨です。

```php
#[\Deprecated]
function test() {
}

#[\Deprecated("use test() instead", since: "2.4")]
function test3() {
}
```

非推奨を表すアトリビュートで、関数、メソッド、クラス定数などに指定可能です。
これを書いておくと、呼び出したときに[E_USER_DEPRECATED ](https://www.php.net/manual/ja/errorfunc.constants.php)が発生します。
その点ではPHPDocの[@Deprecated](https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/deprecated.html)より強制力を持っているといえますね。

他のアトリビュートって実際にユーザランドではあんまり使いそうにないものが多いわけですが、これに限っては出番が多くなりそうです。


## [new MyClass()->method() without parentheses](https://wiki.php.net/rfc/new_without_parentheses)

賛成25反対4で受理。

[括弧を一個減らせます](https://qiita.com/rana_kualu/items/5f34ced66558a4f94a4b)。

```php
// PHP8.3
(new HOGE())->run();

// PHP8.4
new HOGE()->run();
```

インスタンス化からのメソッド即時実行は必ず`(new HOGE())->run();`のように括弧で囲まないといけません。
これは括弧を書かないと`new (HOGE()->run());`と解釈されるからです。

が、newの場合は`(new HOGE())->run();`と解釈しても文法が曖昧にならないことがわかったので優先順位を変更します。
個人的には[わりとよくこの書き方をする](https://qiita.com/rana_kualu/items/38df213f5e30326a81a8)ので、ちょっとだけ楽になりそうです。


## [Correctly name the rounding mode and make it an Enum](https://wiki.php.net/rfc/correctly_name_the_rounding_mode_and_make_it_an_enum)

賛成34反対0で受理。

[round](https://www.php.net/manual/ja/function.round.php)関数の丸めモードを定数からENUMにしようという提案。

```php
// PHP8.3
round(12.34, 1, PHP_ROUND_HALF_UP);

// PHP8.4
round(12.34, 1, RoundingMode::HalfAwayFromZero);
```

名前もより説明的になり、特にマイナス値だった場合の挙動がわかりやすくなります。

```php
enum RoundingMode{
    case HalfAwayFromZero; // PHP_ROUND_HALF_UP
    case HalfTowardsZero;  // PHP_ROUND_HALF_DOWN
    case HalfEven;         // PHP_ROUND_HALF_EVEN
    case HalfOdd;          // PHP_ROUND_HALF_ODD
    case TowardsZero;
    case AwayFromZero;
    case NegativeInfinity;
    case PositiveInfinity;
}
```

既存の定数を削除する予定は今のところありません。

なお後ろ4つは↓で新しく追加された丸めモードです。


## [New 4 rounding modes to round() function](https://wiki.php.net/rfc/new_rounding_modes_to_round_function)

賛成19反対0で受理。

[round](https://www.php.net/manual/ja/function.round.php)関数の丸めモードが4種類追加されます。

```php
round(-12.34, 1, RoundingMode::PositiveInfinity); // 切り上げ
round(-12.34, 1, RoundingMode::NegativeInfinity); // 切り捨て
round(-12.34, 1, RoundingMode::TowardsZero);      // 0に近づける切り捨て
round(-12.34, 1, RoundingMode::AwayFromZero);     // 0から離れる切り上げ
```

切り捨て/切り上げの専用関数[ceil](https://www.php.net/manual/ja/function.ceil.php)/[floor](https://www.php.net/manual/ja/function.floor.php)は桁数を指定できないので、こっちのほうが機能的にも優れていたりします。

これら新しい丸めモードには定数が用意されず、ENUMからのみアクセス可能となります。
実はこのRFCの時点では丸めモードごとに`PHP_ROUND_TOWARD_ZERO`のような定数が用意されていたのですが、こちらが受理された後で↑のRFCも受理されたので、定数は日の目を見ることなく消滅しました。


## [Deprecate implicitly nullable parameter types](https://wiki.php.net/rfc/deprecate-implicitly-nullable-types)

賛成26反対0で受理。

[型のある引数のデフォルトnullが禁止](https://qiita.com/rana_kualu/items/07f829ff763c6a24dd92)されます。

```php
function foo(int $x = null){}

// PHP8.3 OK
// PHP8.4 E_DEPRECATED
// PHP9 エラー
```

どういうことかというと、関数fooの型はintなので本来はintしか渡すことができません。
しかしここに`=null`とデフォルト値が書いてある場合に限り、`$x`にnullを入れることができてしまうのです。
これは元々PHPにnull許容型の引数がなかったからという歴史的経緯です。

ということで、今後は`$x`がintであることが保証されます。

ところで今後、引数はintもしくは渡さないってしたいときはどうすればいいんだろう？

```php
function foo(?int $x){}

foo(1);    // OK
foo(null); // OK
foo();     // Fatal error ArgumentCountError Too few arguments


function foo(?int $x = null){}
foo(1);    // OK
foo(null); // OK
foo();     // OK
```

引数なしはOKだけどnullはエラーにする方法がよくわかりませんでした。


## [Adding bcround, bcfloor and bcceil to BCMath](https://wiki.php.net/rfc/adding_bcround_bcfloor_bcceil_to_bcmath)

賛成13反対0で受理。

[BCMath](https://www.php.net/manual/ja/book.bc.php)に丸め関数を導入します。

```php
bcround('0.285', 2, PHP_ROUND_HALF_UP); // "0.29"
bcfloor('13.0915'); // "13"
bcceil('24.00001'); // "25"
```

PHPにはBCMathと[GMP](https://www.php.net/manual/ja/book.gmp.php)という2種類の任意精度演算があります。
GMPは整数しか使えないけどBCMathは小数も使えるので高機能、かと思いきや実はBCMathのほうが何故か機能が少ないです。
あと遅いらしい。

ということでPHP8.4ではBCMathに[だいぶ高速化の梃入れが入る](https://qiita.com/SakiTakamachi/items/8510ff59b09592ceb51d#bcmath-is-very-slow)予定です。


## [Make the GMP class final](https://wiki.php.net/rfc/gmp-final)

賛成28反対0で受理。

[GMPクラス](https://www.php.net/manual/ja/class.gmp.php)をfinalにします。

PHPでは、[リソース](https://www.php.net/manual/ja/language.types.resource.php)という特殊な型が[片っ端からObjectに置き換えられ](https://wiki.php.net/rfc/resource_to_object_conversion)つつあります。

この置き換えは普通の新規クラス導入と異なり、文法やクラスの扱いがやや特殊です。
たとえば[XMLParser](https://www.php.net/manual/ja/book.xml.php)は[PHP8.0でリソースからObjectに変更された](https://qiita.com/rana_kualu/items/c110cb244c3ee38c6859#xml)のですが、その[使い方](https://www.php.net/manual/ja/function.xml-parse.php)は`$xml->parse($data);`ではなく`xml_parse($xml, $data);`です。
[XMLParser](https://www.php.net/manual/ja/class.xmlparser.php)はfinalクラスであり、直接newすることができず、キャストもシリアライズもできないという変なクラスです。

どうしてこのようなことになっているのかというと、置き換えの前後で互換性を壊さず全く同じ書き方ができるようにしたからです。
実際[is_resource()](https://www.php.net/manual/ja/function.is-resource.php)とかで型チェックでも入れていない限り、XMLParserを使っているソースコードはPHP8.0の前後でそのまま動作します。

GMPもかつてリソースだったものが[PHP5.6でObjectに置き換えられた](https://wiki.php.net/rfc/operator_overloading_gmp)のですが、GMPは上記のような移行方針が固まる前に移行されたものであるため(というかGMPが最初)他の置き換えクラスと形が揃っていません。
GMPクラスはfinalではなく、直接newが可能で、キャストもシリアライズも可能です。

ということで、GMPをせめてfinalにしようというRFCが提出されました。

GMPは演算子オーバーロードが実装されていて色々と特殊なので、extendsできないようにするのはまあ妥当だと思います。
がしかし、今までずっとそのままだったのにどうして今さらこんな話が出てきたのかというと、[このスレッド](https://externals.io/message/123872)・[このRFC](https://wiki.php.net/rfc/operator_overrides_lite)が原因です。
こちらはGMPを明示的にextends可能にするRFCであり、ほぼ演算子オーバーロードのユーザランド解放と同義です。
PHPでは[演算子オーバーロード](https://wiki.php.net/rfc/userspace_operator_overloading)は[様々な問題があって](https://externals.io/message/108788#108805)導入には慎重な姿勢です。
他の置き換えクラスと足並みを揃えるという理由ももちろんあるでしょうが、危険なRFCを事前に封じておこうという魂胆も見え隠れしますね。


## [Lazy Objects](https://wiki.php.net/rfc/lazy-objects)

賛成25反対4で受理。

遅延初期化です。

遅延初期化ってなにかというと、`new XXX()`とか書いてあっても必要になるまで実際はインスタンスを生成しないというものです。
インスタンス生成は比較的重い処理なので、できるだけやりたくないし、やるにしてもなるべく後回しにしたいわけですね。
アイデアとしては[引数のコピーオンライト](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%94%E3%83%BC%E3%82%AA%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%88)と似たようなものです。

ただ現在使っている普通のクラスが遅延初期化されるわけではなく、使用するには専用の構文が必要です。
そのまま遅延初期化されると明らかに互換性がなくなりますしね。

この機能は基本的にユーザが使うものではなく、[Fiber](https://qiita.com/rana_kualu/items/6247441f6fdd63c10a06)と同じようにライブラリやフレームワークが中で使うものになるでしょう。

```php
class MyClass
{
    public function __construct(private int $foo)
    {
        // 重い処理
    }
}

$initializer = static function (MyClass $ghost): void {
    $ghost->__construct(123);
};
$reflector = new ReflectionClass(MyClass::class);
// $objectが遅延初期化オブジェクト
$object = $reflector->newLazyGhost($initializer);
```

Symfonyでは既に[独自実装をLazyObjectsに置き換えたバージョン](https://github.com/nicolas-grekas/symfony/pull/44)で動作検証ができているそうです。
このRFCの提案者はSymfonyのCotribute数2位の人なので、まず間違いないでしょう。


## [Grapheme cluster for str_split function: grapheme_str_split](https://wiki.php.net/rfc/grapheme_str_split)

賛成19反対0で受理。

絵文字を正しく分割できる関数[grapheme_str_split](https://qiita.com/rana_kualu/items/007a5438b2d477c34502)を導入します。

```php
grapheme_str_split("👨‍👩‍👦‍👦"); // ['👨‍👩‍👦‍👦']

mb_str_split("👨‍👩‍👦‍👦"); // ['👨', '‍', '👩', '‍', '👦', '‍', '👦']
str_split("👨‍👩‍👦‍👦"); // ['�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�']
```

文字コードは[魔境すぎて](https://github.com/ufcpp/GraphemeSplitter/blob/master/GraphemeSplitter/Character.GetGraphemeBreakPropertyV10.cs)さっぱりだ。


## [Multibyte for trim function mb_trim, mb_ltrim and mb_rtrim](https://wiki.php.net/rfc/mb_trim)

賛成15反対0で受理。

[マルチバイト版trim](https://qiita.com/rana_kualu/items/87bc5dd0396103bd7a32)です。

```php
mb_trim('　あ '); // "あ"
```

[trim](https://www.php.net/manual/ja/function.trim.php)では無理だった全角スペースが消えてくれるようになります。
日本人としてはありがたい対応ですね。


## [Multibyte for ucfirst and lcfirst functions](https://wiki.php.net/rfc/mb_ucfirst)

賛成15反対0で受理。

マルチバイト版[ucfirst](https://www.php.net/manual/ja/function.ucfirst.php)です。

```php
mb_ucfirst('ψη'); // "Ψη"
ucfirst('ψη');    // "ψη"
```

なおアルゴリズムは[mb_strtoupper](https://www.php.net/manual/ja/function.mb-strtoupper.php)と同じであり、日本語には効きません。
残念ですね。

```php
mb_ucfirst('ぁ'); // "ぁ"
```


## [Add http_(get|clear)_last_response_headers() function](https://wiki.php.net/rfc/http-last-response-headers)

賛成15反対1で受理。

HTTPレスポンスヘッダを取得する関数[http_get_last_response_header()](https://qiita.com/rana_kualu/items/f16cf9757139fcba4191)です。

現在[file_get_contents()](https://www.php.net/file_get_contents)を使うと唐突に[$http_response_header](https://www.php.net/manual/ja/reserved.variables.httpresponseheader.php)という変数が生えてくるのですが、わかりにくいので同じ内容を取得する変数を追加します。

```php
file_get_contents('xxx');

var_dump(http_get_last_response_header()); // $http_response_headerと同じ
```

このRFCでは`$http_response_header`については何もしません。
今後PHP9かその次あたりで非推奨・削除になることでしょう。


## [RFC1867 for non-POST HTTP verbs](https://wiki.php.net/rfc/rfc1867-non-post)

賛成23反対1で受理。

リクエストボディを取得する関数[request_parse_body()](https://qiita.com/rana_kualu/items/8a95669ebbfd19e27da9)です。

現在のPHPでは、POSTだとリクエストボディが`$_POST`に入ってきます。
が、その他のリクエストメソッド`GET`や`PUT`等の場合`$_POST`は空です。

そして何気にこれまでPHPには、リクエストボディを直接いいかんじに取ってくる方法がありませんでした。
かなり意外ですね。

ということでリクエストボディを取得する関数を実装します。

```php
[$_POST, $_FILES] = request_parse_body();
```

これでリクエストメソッドに関係なく、リクエストボディを簡単に取得できるようになります。


## [A new JIT implementation based on IR Framework](https://wiki.php.net/rfc/jit-ir)

賛成25反対0で受理。

[中間表現を用いた新たなJIT](https://qiita.com/rana_kualu/items/d33ed0f770015d91b72e)を実装します。

なんとこのために、[JITフレームワークを一から作り上げました](https://github.com/dstogov/ir)。
機能は現在のJITと全く同じで、動作は1割弱ほど高速化し、コンパイル速度は最大4倍になるそうです。
いったいどういうことだってばよ。
さらに、PHPのために作ったものですが、PHPに依存はしておらず他言語からも利用可能だということです。

PHP8.0でのJITの実装は[900コミット5万行の追加](https://github.com/php/php-src/pull/3792)というヤバい規模でしたが、今回は[250コミット7万行](https://github.com/php/php-src/pull/12079)です。
[この人](https://github.com/dstogov)のバイタリティどうなってんだ。


## [Change how JIT is disabled by default](https://wiki.php.net/rfc/jit_config_defaults)

賛成17反対0で受理。

JITを無効化する方法を変更します。

現在、JIT関連設定のデフォルト値はこうなっています。

```php
opcache.jit=tracing
opcache.jit_buffer_size=0
```

現在JITを無効化する方法は`opcache.jit_buffer_size=0`です。
なんというかわかりにくいのでこうしましょう。

```php
opcache.jit=disable
opcache.jit_buffer_size=64m
```

有効無効を普通にdisableで指定できるようにします。

これはデフォルト値を変更するだけなので、既存ユーザには影響はありません。
明らかにこちらのほうがわかりやすいですね。


## [Increasing the default BCrypt cost](https://wiki.php.net/rfc/bcrypt_cost_2023)

賛成25反対0で受理。

[PASSWORD_BCRYPTのコストのデフォルト値](https://www.php.net/manual/ja/password.constants.php#constant.password-bcrypt)は10で、これは[password_hashが実装された11年前](https://wiki.php.net/rfc/password_hash)からずっとそのままです。

当時からCPUのパワーもだいぶ上がったので、コストを上げようという提案です。
二次投票でデフォルト値11が12票、デフォルト値12が14票となり、[デフォルト値は12](https://github.com/php/php-src/pull/12367)になりました。

ハッシュアルゴリズムを変えたらこれまでのユーザがログインできなくなるのでは？と思うかもしれませんが、[password_hash](https://www.php.net/manual/ja/function.password-hash.php)の特徴のひとつが完全な互換性であり、既存ユーザの認証には全く影響ありません。


## [PDO driver specific sub-classes](https://wiki.php.net/rfc/pdo_driver_specific_subclasses)

賛成23反対0で受理。

[PDOサブクラス](https://qiita.com/rana_kualu/items/6a9d82c8764476839b23)です。

```php
// MySQL固有機能が使える
$pdo = new PdoMySql('mysql:host=localhost;dbname=test');
$pdo->getWarningCount();

// PostgreSQL固有機能が使える
$pdo = new PdoPgsql('pgsql:host=localhost;dbname=test');
$pdo->escapeIdentifier($name);

// SQLite固有機能が使える
$pdo = new PdoSqlite('sqlite:/path/to/sqlite_db.db');
$pdo->createCollation($collection);
```

固有機能を使う必要がない場合は、これまでどおり`new PDO()`としておけば何も変わりません。

こちら、元々PHP8.3向けで受理されたのですが、[担当者が病気](https://github.com/php/php-src/pull/11740#issuecomment-1642200421)のため対応できなくなってしまいました。
その後、[他のContributorたちの努力](https://github.com/php/php-src/pull/12804)によって対応され、無事PHP8.4にマージされました。


## [PDO Driver specific SQL parser](https://wiki.php.net/rfc/pdo_driver_specific_parsers)

賛成27反対0で受理。

PDOのSQLパーサは、歴史的理由によりMySQLを基準として作られています。

そんなわけで[Posgres](https://bugs.php.net/bug.php?id=78534)や[SQL Server](https://bugs.php.net/bug.php?id=79276)などでSQLを正しくパースできないことがあるという問題が発生していました。
そこでSQLドライバごとに個々のパーサを用意します。

むしろ今までパーサひとつでやってたことにびっくりだよ。


## [DOM HTML5 parsing and serialization](https://wiki.php.net/rfc/domdocument_html5_parser)

賛成16反対0で受理。

[PHPのDOM](https://www.php.net/manual/ja/book.dom.php)はHTMLのパースに[libxml](http://www.xmlsoft.org/)を使用しています。
これがHTML4までしか対応していないので、HTML5を正しくパースすることができません。
[libxmlにIssueも立っています](https://gitlab.gnome.org/GNOME/libxml2/-/issues/211)が対応は遅々として進んでいません。

ということで最新のHTMLにも対応したパーサ[Lexbor](https://github.com/lexbor/lexbor)を使用した新たなクラス`\DOM\HTMLDocument`を導入します。
互換性のため、既存の[DOMDocument](https://www.php.net/manual/ja/class.domdocument.php)を置き替えるのではなく新たなクラスを作ることにしたようです。

RFCはだいぶ長いので、詳しくは直接見てください。


## [Opt-in DOM spec-compliance](https://wiki.php.net/rfc/opt_in_dom_spec_compliance)

賛成14反対0で受理。

PHPのDOMは[DOM Core Level 3 specification](https://www.w3.org/TR/DOM-Level-3-Core/)仕様に従っています。
しかし現在既にDOMの仕様は[Living Specification](https://dom.spec.whatwg.org/)に移行しており、さらに既存のDOM実装には多くのバグ(というかDOM Core Level 3仕様の不備による動作の差異)が含まれています。

```php
$dom = new DOMDocument;
$outer = $dom->appendChild($dom->createElementNS("urn:a", "outer"));
$outer->appendChild($dom->createElement("inner"));
echo $dom->saveXML();
```

これは`<outer xmlns="urn:a"><inner xmlns=""/></outer>`になるべきですが、PHPではDOMが導入された5.0から8.3までずっと`<outer xmlns="urn:a"><inner/></outer>`になります。
もはや深く根付いてしまっており、今さらこの挙動を変えることは不可能でしょう。

ところでちょうど`DOM\Document`のRFCが受理されたので、新しいDOMではついでにこのようなバグも全部修正してしまおうということになりました。
結果として`DOM\Node`と`DOMNode`の動作が変わることになるので、ついでに古いDOMを新しい方にインポートするメソッドも用意します。

```php
class DOM\Document {
    public function importLegacyNode(DOMNode $node, bool $deep = false): DOM\Node;
}
```


DOMの[既存バグ](https://wiki.php.net/rfc/opt_in_dom_spec_compliance#bug_list)が思ったよりたくさんあってびっくりだよ。
まあ大半はnullを返すべきところでfalseを返しているなど昔のPHPだなあってやつではありますが、たまに深刻なやつもあったりします。


## [New ext-dom features in PHP 8.4](https://wiki.php.net/rfc/dom_additions_84)

賛成25反対0で受理。

ここまでで既にDOMがだいぶ変更されていますが、ついでだからということでさらなる改善を加えます。

```php
// querySelector
$dom->querySelector('p ~ span')->textContent;

// closest
$dom->getElementById('div3')->closest('div')->getAttribute("xml:id");

// matches
$dom->getElementById('div3')->matches('div > div');
```

CSSのセレクタ、[querySelector](https://developer.mozilla.org/ja/docs/Web/API/Document/querySelector)・[closest](https://developer.mozilla.org/ja/docs/Web/API/Element/closest)・[matches](https://developer.mozilla.org/ja/docs/Web/API/Element/matches)の輸入です。
これは便利。

ほかにも現在PHPに存在しない[Dom\TokenList](https://dom.spec.whatwg.org/#interface-domtokenlist)を追加しようといった様々な改善がなされています。

正直だいぶ使い辛かったDOMが、かなり便利になりそうですね。


## [XML_OPTION_PARSE_HUGE](https://wiki.php.net/rfc/xml_option_parse_huge)

賛成11反対0で受理。

DOMが使用しているlibxml2は、バージョン2.7.0で[大きなデータの入力を拒否](https://github.com/GNOME/libxml2/commit/8915c150b5630178b0f9e83f0d911090095b58a1)するようになりました。
これはDOS攻撃を防ぐためのものです。
で、同時にその上限を許可するオプション`XML_PARSE_HUGE`も用意されまして、[PHPからも使用可能](https://www.php.net/manual/ja/libxml.constants.php#constant.libxml-parsehuge)です。

ところがこのオプション、DOMやsimplexmlには効くのですが、何故か[xml_parse](https://www.php.net/xml_parse)には使えませんでした。
ということで[XMLパーサ関数](https://www.php.net/manual/ja/ref.xml.php)でもこのオプションを使えるようにする提案です。

```php
$parser = xml_parser_create();
xml_parser_set_option($parser, XML_OPTION_PARSE_HUGE, true);

$success = xml_parse($parser, $でかいXMLファイル);
```


## [Improve callbacks in ext/dom and ext/xsl](https://wiki.php.net/rfc/improve_callbacks_dom_and_xsl)

賛成15反対0で受理。

[DOMXPath](https://www.php.net/manual/en/class.domxpath.php)の改善です。

```php
// registerPhpFunctionsに連想配列を渡す
$xpath->registerPhpFunctions([
  "function_name" => $callable
]);

// registerPhpFunctionNSを追加
$xpath->registerPhpFunctionNS("http://example.com", "first_function", $callable);
```

実はDOMもXPathも全く使っていないので、これがどう便利なのかよくわかりませんでした。
誰かがコメントで親切に解説してくれるはず。


## [Add stream open functions to XML{Reader,Writer} (was: Add openStream() to XML{Reader,Writer})](https://wiki.php.net/rfc/xmlreader_writer_streams)

賛成13反対0で受理。

[XMLReader](https://www.php.net/manual/ja/book.xmlreader.php)および[XMLWriter](https://www.php.net/manual/ja/book.xmlwriter.php)にストリーム読み書きのメソッドを追加します。

```php
$stream = fopen('path/to/file', 'w+');

// 読み込み
$reader = XMLReader::fromStream($h);

while ($reader->read()) {
    switch ($reader->nodeType) {
        case XMLReader::ELEMENT:
            echo "Element: ", $reader->name, "\n";
            break;
        case XMLReader::COMMENT:
            echo "Comment: ", $reader->value, "\n";
            break;
    }
}

// 書き込み
$writer = XMLWriter::toStream($h);

$writer->startElement("root");
$writer->writeAttribute("align", "left");
$writer->writeComment("hello");
$writer->endElement();
$amount = $writer->flush();
```

XMLReader/Writerは内部的にはストリーム的な処理をやっているのに、`fromStream`や`toStream`がありませんでした。
そのせいで一度文字列にしてから投入するなどの手間が必要でした。

今後はストリームとXMLReader/Writerを直接行き来できるようになります。


## [Dedicated StreamBucket class](https://wiki.php.net/rfc/dedicated_stream_bucket)

賛成20反対0で受理。

ストリームフィルタ用クラスStreamBucketを導入します。

```php
class simple_filter extends php_user_filter {
    function filter($in, $out, &$consumed, $closing) {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}
 
stream_filter_register("simple", "simple_filter")
```

PHPにはストリームに一律で処理を行える[ストリームフィルタ](https://qiita.com/rana_kualu/items/dc99be34b9e2f721b70f)という機能があります。
しかしこれ、[生成されるオブジェクト](https://www.php.net/manual/ja/function.stream-bucket-make-writeable.php)が何故か`stdClass`なんですよね。

ということで`StreamBucket`クラスを作ろうという提案です。

```php
final class StreamBucket
{
    /**
     * @var resource
     * @deprecated リソースからオブジェクトへ移行が終わったら
     */
    public $bucket;
    public string $data;

    /** @deprecated 次バージョン */
    public int $datalen;
    public int $dataLength;
}
```

また、プロパティ`datalen`は現在のPHP命名規則と合っていないので、中身の同じプロパティ`dataLength`を作り、今後どこかで`datalen`を削除します。

むしろどうして今まで`stdClass`だったのか謎。


## [Unbundle the imap, pspell, and oci8 extensions](https://wiki.php.net/rfc/unbundle_imap_pspell_oci8)

賛成27反対0で受理。

問題を抱えているいくつかの拡張機能をバンドルから外します。

[ext/imap](https://www.php.net/manual/ja/book.imap.php)は内部で使っているライブラリ[uw-imap/imap](https://github.com/uw-imap/imap)最終更新が2019年で、OAuthに対応していないのでGmail等に使えず存在価値がなくなりました。
送信だけなら[PHPMailer](https://github.com/PHPMailer/PHPMailer)使っとけばいいし、受信も必要であれば[webklex/php-imap](https://packagist.org/packages/webklex/php-imap)などの代替があります。

[ext/pspell](https://www.php.net/manual/ja/book.pspell.php)は、[使っている辞書](https://sourceforge.net/projects/pspell/)の最終更新が2001年です。

[ext/oci8](https://www.php.net/manual/ja/book.oci8.php)の[Oracleのライブラリ](https://www.oracle.com/database/technologies/instant-client.html)は非常に多くのバグがあます。
[PHP側の最近の修正](https://bugs.php.net/search.php?cmd=display&package_name[]=PDO+OCI&package_name[]=OCI8+related&package_name[]=Oracle+related&order_by=ts1&direction=ASC&limit=30&status=Open&reorder_by=ts1)は、ほとんどが通らないテストをスキップするようにしただけです。

ということでこれらはPHP本体に同梱されなくなります。
バンドルから外されてもPECLには残るので、当面はPECLからインストール可能です。


## [Raising zero to the power of negative number](https://wiki.php.net/rfc/raising_zero_to_power_of_negative_number)

賛成27反対0で受理。

0のマイナス乗をエラーにします。

```php
var_dump(0 ** -1);
// PHP8.3 float(INF)
// PHP8.4 float(INF) Deprecated: Zero raised to a negative power is deprecated
// PHP9.0 DivisionByZeroError
```

`x`のマイナス乗は`1/x`のプラス乗です。
`10 ** -3` === `1/10 ** 3`ということですね。

つまり0をマイナス乗すると`1/0 ** 1`になるわけで、従ってゼロ除算エラーにならないのはおかしいというわけです。

あとついでに、[fmod](https://www.php.net/fmod)に対応する算術演算関数[fpow](https://www.php.net/fpow)を追加します。


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

賛成21反対0で受理。

[便利な配列検索関数array_find](https://qiita.com/rana_kualu/items/6acf9064a55d7f50b4b8)を追加します。

```php
$array = [
    'a' => 'dog',
    'b' => 'cat',
    'c' => 'cow',
    'd' => 'duck',
    'e' => 'goose',
    'f' => 'elephant'
];
 
// 検索して最初の値を返す
var_dump(array_find($array, function (string $value) {
    return strlen($value) > 4;
})); // string(5) "goose"

// 検索して最初のキーを返す
var_dump(array_find_key($array, function (string $value, $key) {
   return preg_match('/^([a-f])$/', $key);
})); // string(1) "a"

// ひとつでもtrueであればtrue
var_dump(array_any($array, function (string $value) {
    return strlen($value) < 3;
})); // bool(false)

// 全項目がtrueであればtrue
var_dump(array_all($array, function (string $value, $key) {
   return is_string($key);
})); // bool(true)
```


配列の各項目を検索する関数が4つ追加されます。
一致する最初の値を返す`array_find`、一致する最初のキーを返す`array_find_key`、ひとつでも当てはまる項目が存在するかチェックする`array_any`、全て項目が当てはまるかをチェックする`array_all`です。

便利ですね。


# 感想

PHP8.4のα版は既に提供されています。
[Linux](https://downloads.php.net/~saki/) / [Windows](https://windows.php.net/qa/)

最も影響のありそうな変更点と言えば、やはりプロパティフックと非対称可視性ですね。
開発当初のPHPはプロパティへのアクセスがフリーパスだったわけですが、時代の移り変わりとともにどんどんプロパティ周りの機能が追加されてきました。
型宣言、NULL許容型、readonly、UNION型、交差型、[オブジェクト初期化子](https://qiita.com/rana_kualu/items/122a1da3b6089b125cb0)、[型パズル](https://qiita.com/rana_kualu/items/ddadd32756afdefbe99f)、そして今回のプロパティフックに非対称可視性。
今となっては、もう下手な言語より充実していると言って過言ではないでしょう。