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

[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)

**[2023/11/23にリリースされました](https://qiita.com/rana_kualu/items/f03efb9810c3ac5b2ab6)。**


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

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


# RFC

## [Marking overridden methods (#[\Override])](https://wiki.php.net/rfc/marking_overriden_methods)

賛成22反対1で受理。

[Overrideアトリビュート](https://qiita.com/rana_kualu/items/938e696c9ce86f5d5428)です。

```php
class C1 {
    protected function foo(): void {}
}

class C2 extends C1 {
    #[\Override]
    public function foo(): void {} // OK
}

class C3 extends C1 {
    #[\Override]
    public function bar(): void {} // NG
}
```

C3はコンパイルエラーになるので、実装のミスや、親が変更されたときに気付くことができます。


## [Typed Class Constants](https://wiki.php.net/rfc/typed_class_constants)

賛成25反対0で受理。

[クラス定数に型が書ける](https://qiita.com/rana_kualu/items/f04f687b3f4e7d11e6b6)ようになります。

```php
enum E {
    const string TEST = "Test1";
}

trait T {
    const string TEST = E::TEST;
}

interface I {
    const string TEST = E::TEST;
}

class C implements I{
    use T;
    const string TEST = E::TEST;
}
```

わかりやすい！



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

賛成15反対1で受理。

[マルチバイト対応版str_pad](https://qiita.com/rana_kualu/items/b2967c863cc882d072e7)です。

```php
$str = 'あ';
var_dump(str_pad($str, 2, 'い')); // あ
var_dump(mb_str_pad($str, 2, 'い')); // あい
```

うっかり日本語に[str_pad](https://www.php.net/manual/ja/function.str-pad.php)を使うと変な記号になったりpadできなかったりしたのですが、今後は思ったとおりのパディングになってくれます。


## [Dynamic class constant fetch](https://wiki.php.net/rfc/dynamic_class_constant_fetch)

賛成15反対4で受理。

[文字列からクラス定数を取得](https://qiita.com/rana_kualu/items/d0dc17543f4b89db30cb)できるようになります。

```php
$foo->$bar;    // OK
Foo::${$bar};  // OK
Foo::{$bar}(); // OK

Foo::{$bar};   // PHP8.3以降OK
```

他の項目はだいたい文字列でキーを指定する方法があるのですが、なぜかクラス定数にはそれができませんでした。
今後はクラス定数にもこの書き方ができるようになります。

また、この機能の導入に伴い、ENUMの使い勝手が大幅によくなりました。

```php
enum Suit:string{
    case Hearts = 'ハート';
    case Diamonds = 'ダイヤ';
    case Clubs = 'クラブ';
    case Spades = 'スペード';
}

echo Suit::{$_REQUEST['suit']}?->value; // ハート
```

べんり。


## [Arbitrary static variable initializers](https://wiki.php.net/rfc/arbitrary_static_variable_initializers)

賛成25反対0で受理。

[static変数に変数や関数を突っ込める](https://qiita.com/rana_kualu/items/20b80ded17c77e2b05d4)ようになります。

```php
function foo(int|null $start){
    static $i = $start; // PHP8.3以降OK
    echo $i++;
}

foo(5); // 5
foo();  // 6
```

これまでstatic変数の初期化には固定値しか入れられなかったのですが、変数や関数が設定できるようになりました。
なお、特に技術的理由や意図的に固定値しか設定できないようにしていたわけではなく、なんとなく昔からそうなっていただけみたいです。


## [Readonly amendments](https://wiki.php.net/rfc/readonly_amendments)

賛成26反対0で受理。

readonlyプロパティをcloneするときに再初期化できるようになりました。

```php
class Foo {
    // コンストラクタ
    public function __construct(
        public readonly DateTime $bar,
        public readonly DateTime $baz
    ) {}

    // clone
    public function __clone()
    {
        $this->bar = clone $this->bar; // OK
        $this->cloneBaz();
    }
    private function cloneBaz()
    {
        unset($this->baz); // __cloneから呼ばれている場合はOK
    }
}

$foo = new Foo(new DateTime(), new DateTime());
$foo2 = clone $foo;
```

マジックメソッド`__clone()`内において、readonlyプロパティを一回だけ変更できるようになります。

私には正直これのメリットがよくわからないのですが、全員賛成という投票結果からすると何らかの大きな利点があるのでしょう。


## [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
$pdoMySQL = new PdoMySql($dsn);
$pdoMySQL->getWarningCount(); // MySQL専用機能

// Postgres
$pdoPgsql = new PdoPgsql($dsn);
$pdoMySQL->getPid(); // Postgres専用機能

// ファクトリーメソッド
$pdo = PDO::connect($dsn); // DSNがMysQLならPdoMySqlに、PostgresならPdoPgsqlになる

// PDOはこれまでと同じ
$pdo = new PDO($dsn);
```

どうして今まで存在しなかったのだろうって思いますねこれ。

データベースに固有の機能を使えるようになります。
そのかわり移植性は下がりますが、どうせDB移植なんて滅多にやんないし利便性のほうが勝るでしょう。

もちろんこれまでの`new PDO()`も使い続けることもできて、その場合はこれまでと一切変わりません。


## [Randomizer Additions](https://wiki.php.net/rfc/randomizer_additions)

[乱数の機能追加](https://qiita.com/rana_kualu/items/0c6bb76736c952a5e217)です。

```php
$randomizer = new \Random\Randomizer();

// ランダムな文字列を生成する
$randomizer->getBytesFromString('abcdefghijklmnopqrstuvwxyz0123456789', 16);

// aが75%、bが25%のランダム文字列を生成する
$randomizer->getBytesFromString('aaab', 16);

// (-180, 180] の乱数を生成する
$randomizer->getFloat(-180, 180, \Random\IntervalBoundary::OpenClosed);

// [0, 1)の乱数を生成する
$randomizer->nextFloat();
  // $randomizer->getFloat(0, 1, \Random\IntervalBoundary::ClosedOpen)と同じ
```

暗号学的に安全なランダム文字列や、範囲指定の乱数を簡単に作れるようになりました。
開区間閉区間も指定可能です。
これはたいへん使い勝手が良いですね。


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

賛成18反対1で受理。

[JSONが正しいかどうか判定できる関数](https://qiita.com/rana_kualu/items/e293a87342a94caf52a6)が追加されます。

```php
var_dump(json_validate('{ "": "": "" } }')); // false
```

[json_decode](https://www.php.net/manual/ja/function.json-decode.php)に突っ込めば正しいかわかるから[いらんだろ](https://externals.io/message/118310)との声も上がっていたのですが、丁寧な説得と様々なユースケースの提供によって、みごとに採択まで行きつきました。



## [Define proper semantics for range() function](https://wiki.php.net/rfc/proper-range-semantics)

賛成20反対0で受理。

[range()](https://www.php.net/range)関数の修正およびハンドリング機能強化です。

rangeにあまり一般的でない引数の渡し方をした場合、不自然な挙動になることがありました。
そのような場合もなるべく自然な動作になるようにし、また不正な引数にはエラーを出すようにします。

```php
var_dump(range(0, 3, -1));
// PHP8.2まで [0, 1, 2, 3]
// PHP8.3以降 ValueError

var_dump(range('9', 'A'));
// PHP8.2まで [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
// PHP8.3以降 [9, :, ;, <, =, >, ?, @, A]

var_dump(range('', 0));
// PHP8.2まで [0]
// PHP8.3以降 [0]  Warning: range(): Argument #1 ($start) must not be empty, casted to 0
```

そもそもこれまで`$step`の-1が場合によっては無視されていたってのが衝撃だな。


## [More Appropriate Date/Time Exceptions](https://wiki.php.net/rfc/datetime-exceptions)

賛成25反対1で受理。

[DateTimeのエラーハンドリング機能強化](https://qiita.com/rana_kualu/items/342aba05b612f5b237e2)です。

```php
$i = DateInterval::createFromDateString('last Monday of');
$sub = (new DateTimeImmutable())->sub($i);

// PHP8.2  Warning: DateTimeImmutable::sub(): Only non-special relative time specifications are supported for subtraction
// PHP8.3  Fatal error: DateMalformedIntervalStringException 
```

何種類かのDateExceptionおよびDateErrorを導入し、細かなバリデーションを行うことができるようになります。


## [Improve unserialize() error handling](https://wiki.php.net/rfc/improve_unserialize_error_handling)

[unserializeのエラーハンドリング機能強化](https://qiita.com/rana_kualu/items/fc1a359b85118b4b7a76)です。
3つの提案があり、うち2つが受理されました。

```php
unserialize('foo');
// PHP8.2  E_NOTICE
// PHP8.3  E_WARNING
// PHP9.0  UnserializationFailedException

unserialize('O:19:"SplDoublyLinkedList":3:{i:0;i:0;i:1;N;i:2;a:0:{}}');
// PHP8.2  UnexpectedValueException
// PHP9.0  UnserializationFailedException
```

unserializeしたとき、これまでE_NOTICEやE_WARNINGやUnexpectedValueExceptionなど様々なエラーや例外が発生していたせいでハンドリングしづらかったのですが、今後はUnserializationFailedExceptionで一括でcatchできるようになります。

なお、UnserializationFailedExceptionの導入自体はPHP9になりました。
PHP8.3ではそのための準備が行われるようです。


## [Make unserialize() emit a warning for trailing data](https://wiki.php.net/rfc/unserialize_warn_on_trailing_data)

賛成20反対0で受理。

[unserialize()](https://www.php.net/unserialize)の末尾に不要な文字列が入っていた場合に警告を出します。

```php
var_dump(unserialize('i:5;i:6;'));

// PHP8.2  エラー出ない
// PHP8.3  unserialize(): Extra data starting at offset 4 of 8 bytes
```

`i:5;i:6;`のパースは`i:5;`で終わっていて、値は`int(5)`になります。
その後ろにある`i:6;`は全く不要な文字であり、これまでは無視されていましたが、今後はE_WARNINGになります。

どうやら[CVE-2023-21036](https://cve.report/CVE-2023-21036)と同じ脆弱性の問題があるとかなんとか。


## [Saner array_(sum|product)()](https://wiki.php.net/rfc/saner-array-sum-product)

賛成27反対0で受理。

[array_sum/array_productの改善](https://qiita.com/rana_kualu/items/b910ceb26be0e5d2a57f)です。

```php
echo gmp_init("123") + 456;
// PHP8.2  579
// PHP8.3  579

echo array_sum([gmp_init("123"), 456]);
// PHP8.2  456 ←
// PHP8.3  579
```

[array_sum](https://www.php.net/manual/ja/function.array-sum.php)や[array_product](https://www.php.net/manual/ja/function.array-product.php)は、これまで一部の動作が算術演算子と異なっていました。
PHP8.3以降は同じ動作になります。


## [Path to Saner Increment/Decrement operators](https://wiki.php.net/rfc/saner-inc-dec-operators)

賛成25反対0で受理。

[インクリメント・デクリメント演算子](https://www.php.net/manual/ja/language.operators.increment.php)を改善します。

```php

$i = 10;
var_dump($i-1); // 9
var_dump(--$i); // 9

$n = null;
var_dump($n-1); // -1
var_dump(--$n); // null

$s = 'foo';
var_dump($s-1); // Fatal error
var_dump(--$s); // "foo"
var_dump($s+1); // Fatal error
var_dump(++$s); // "fop"
```

intとfloat以外の型へのインクリメント・デクリメントは、算術演算子と異なる結果になることがあります。
また、時に想像と斜め上の挙動をすることがあります。
文字列にインクリメントすると次の文字になるけどデクリメントは効かないなんてところは特に意味がわかりませんね(これは移植元のPerlからそういう仕様)。

これを改善し、`++$i`は`$i+1`と同じ挙動になるようにします。
intとfloat以外の型へのインクリメント・デクリメントはPHP8.3でDeprecatedとなり、PHP9で動かなくなります。

また文字列には、インクリメント・デクリメントを行う関数[str_increment()](https://www.php.net/manual/ja/function.str-increment.php)・[str_decrement()](https://www.php.net/manual/ja/function.str-decrement.php)を別途追加します。

```php
$s = 'foo';
var_dump(str_decrement($s)); // "fon"
var_dump(str_increment($s)); // "fop"
```

これまでできなかった文字列デクリメントもできるようになるのはいいですね。


## [Use exceptions by default in SQLite3 extension](https://wiki.php.net/rfc/sqlite3_exceptions)

賛成21反対0で受理。

[SQLite3](https://www.php.net/manual/ja/book.sqlite3.php)拡張モジュールは、[デフォルトでは例外を使わない](https://www.php.net/manual/ja/sqlite3.enableexceptions.php)ようになっています。
しかし[PDO](https://wiki.php.net/rfc/pdo_default_errmode)も[MySQLi](https://wiki.php.net/rfc/mysqli_default_errmode)もとっくにデフォルト例外に移行しているため、SQLiteも例外をデフォルトにすることにします。

```php
var_dump($sqlite3->enableExceptions());
// PHP8.2 false
// PHP8.3 false
// PHP9.0 true
// PHP10.0 Fatal error Undefined method

$sqlite3->enableExceptions(false);
// PHP8.2 OK
// PHP8.3 Deprecated
// PHP9.0 Fatal error
```

まずPHP8.3では`enableExceptions(false)`するとE_DEPRECATEDが発生します。
ただし初期値はfalseのまま変わりません。

PHP9.0では`enableExceptions(false)`することができなくなり、デフォルトもtrueになります。

最後にPHP10では`enableExceptions()`メソッド自体がなくなります。


## [Deprecate remains of string evaluated code assertions](https://wiki.php.net/rfc/assert-string-eval-cleanup)

賛成24反対0で受理。

[assert_options()](https://www.php.net/manual/ja/function.assert-options.php)関数、および関連する定数を削除します。

```php
assert_options(ASSERT_BAIL, 1);

// PHP8.2 エラー出ない
// PHP8.3 Deprecated
```

昔は`assert()`の引数`$assertion`に文字列を渡すとevalされて評価されていたのですが、この使い方はPHP7.2で非推奨となりPHP8.0で削除されました。
しかし機能自体は削除されたものの、関連するini設定が一部残っていました。

また`assert_options()`は元より`ini_set()`を使えとドキュメントに書かれているくらいだったのですが、正式に非推奨となります。


## [Deprecate functions with overloaded signatures](https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures)

[標準関数におけるシグネチャのオーバーロードを廃止](https://qiita.com/rana_kualu/items/4d67ffdf4cb05d1ba86f)します。
なお、ここで言うオーバーロードは[PHPのオーバーロード](https://www.php.net/manual/ja/language.oop5.overloading.php)ではなく、[一般的な意味のオーバーロード](https://ja.wikipedia.org/wiki/%E5%A4%9A%E9%87%8D%E5%AE%9A%E7%BE%A9)です。
どうして同じ言葉にしてしまったのか。

例として[DatePeriod::__construct()](https://www.php.net/manual/ja/dateperiod.construct.php)はシグネチャが3種類もあります。

```php
class DatePeriod
{
    public function __construct(DateTimeInterface $start, DateInterval $interval, DateTimeInterface $end, int $options = 0) {}
    public function __construct(DateTimeInterface $start, DateInterval $interval, int $recurrences, int $options = 0) {}
    public function __construct(string $isostr, int $options = 0) {}
}
```

このうち、一番上のシグネチャだけを残します。
その他のシグネチャはPHP8.3でDeprecateとし、PHP9で削除します。

```php:PHP9
class DatePeriod
{
    public function __construct(DateTimeInterface $start, DateInterval $interval, DateTimeInterface|int $end, int $options = 0) {}
    public static function createFromISO8601String(string $specification, int $options = 0): static {}
}
```

削除されるシグネチャのうち、今後も必要そうなものには代替関数なりメソッドなりが用意されます。

他にもたくさんの関数やメソッドについての提案があり、それぞれ受理されたり却下されたりしています。


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

PHPには、主に歴史的な理由から、時代遅れになった機能、セキュリティ的に問題のある機能、そもそもなんでこんなものが存在するのか機能などが多々存在します。
ということでPHPではバージョンアップのたびにそれら負の遺産を消したり消さなかったりしています。
今回は以下の4機能が、PHP8.3で非推奨となり、PHP9で削除されます。

```php
mb_strimwidth('abcde', 0, -1);
// PHP8.2 abcd
// PHP8.3 abcd Deprecated
// PHP9.0 Fatal error

var_dump(NumberFormatter::TYPE_CURRENCY);
// PHP8.2 値は取れる エラー出ない
// PHP8.3 値は取れる Deprecated
// PHP9.0 Fatal error Undefined constant

ldap_connect($host, $port);
// PHP8.2 エラー出ない
// PHP8.3 Deprecated
// PHP9.0 Fatal error
```

[mb_strimwidth()](https://www.php.net/manual/ja/function.mb-strimwidth.php)の引数`$width`に負の数を与えられなくなります。
幅に負数を与えると末尾からの数を数えるのは文字列関数に共通する挙動ですが、そもそも`mb_strimwidth()`は『文字列を特定長にする』目的で使われるものであり、目的と合わない機能です。
元の文字列長に依存する切り取りを行いたい場合は[mb_substr()](https://www.php.net/manual/ja/function.mb-substr.php)などを使いましょう。

定数[NumberFormatter::TYPE_CURRENCY](https://www.php.net/manual/ja/class.numberformatter.php#intl.numberformatter-constants.types)が削除されます。
この定数は、定義こそされているものの実装されていませんでした。

定数[MT_RAND_PHP](https://www.php.net/manual/ja/function.mt-srand.php)が削除されます。

かつてPHPの乱数には[壊れたメルセンヌツイスター問題](https://zenn.dev/zeriyoshi/articles/abd808d1c6d31b#%E5%A3%8A%E3%82%8C%E3%81%9F-mersenne-twister-%E5%AE%9F%E8%A3%85%E3%81%AE%E5%95%8F%E9%A1%8C)というものが存在しました。
これはPHP7.1で修正されたのですが、互換などの理由で壊れた乱数をあえて再現する手段として定数MT_RAND_PHPが導入されました。

その後PHP8で実は[壊れたメルセンヌツイスターが壊れている](https://github.com/php/php-src/pull/9197)ことが発覚したのですが、どこからも文句が来なかったしもう互換いいんじゃねってなったみたいです。

ちなみに[壊れていない方のメルセンヌツイスターも削除しよう](https://wiki.php.net/rfc/deprecations_php_8_3#global_mersenne_twister)という提案もありましたが、さすがにこちらは却下されました。
とはいえメルセンヌツイスターは暗号論的に安全ではないので、今から新しく使うべきではありません。

[ldap_connect](https://www.php.net/ldap_connect)のシグネチャのうち2引数のほうが削除されます。
今後は1引数のほうを使いましょう。


# 感想

PHP8.3は既にα版が提供されています。
[Linux](https://downloads.php.net/~jakub/) / [Windows](https://windows.php.net/qa/)
締め切り間際で受理された機能などはまだ入ってないものもありますが、既に一部の機能は試すことができるので、遊んでみてはどうしょうか。

[Nikitaの遺産](https://qiita.com/rana_kualu/items/eaf97694f25a62f93a7e)が残っていた[PHP8.2](https://qiita.com/rana_kualu/items/fc4b02e2daaf102aa92f)と異なり、PHP8.3は彼の手はほとんど入っていません。
しかしながらも着実に様々な改善、改良がなされており、[次世代にしっかり引き継がれている](https://qiita.com/rana_kualu/items/0479edbd936447572c3b)のがわかりますね。

今後もPHPはeasyかつsimpleな言語であり続けてほしいものですね。