PHPerのための「PHP8.3 の新機能について語り合う」PHP TechCafe ================== # PHPer's NEWS ::: info **[An Introduction to PXP](https://pxplang.org/blog/an-introduction-to-pxp)** ::: ::: info **[「NativePHP」、PHPでデスクトップアプリを開発できるフレームワークが登場](https://www.publickey1.jp/blog/23/nativephpphp.html)** 現時点で対応する開発環境はMacもしくはLinux… ::: ::: info **[PhpStorm 2023.2 Is Now Available](https://blog.jetbrains.com/phpstorm/2023/08/phpstorm-2023-2-is-now-available/)** AIアシスタントが気になる ::: ::: info **[Microsoft Edgeに閲覧したすべてのページのスクリーンショットを保存する機能が追加へ](https://softantenna.com/blog/microsoft-edge-save-screenshots/)** ::: ::: info **[10年開発してきたPHPアプリケーションにPHPStanを導入した](https://devblog.thebase.in/entry/2023/08/21/131451)** ブログやカンファレンスでも最近PHPStan導入の話をよく見聞きします。 Psalmの話はあまり聞かないけど、シェアどれくらいなんだろう…(日本と世界でも違うかも?) ::: ::: info **[PHP Annotated – July 2023](https://blog.jetbrains.com/phpstorm/2023/08/php-annotated-july-2023/)** elePHPant をデザインされた Vincent Pontier 氏が PHP 9 のロゴを作成 他 ::: ::: info **[8 Code Quality Tools To Use In Your Long-Term PHP Applications](https://davorminchorov.com/8-code-quality-tools-to-use-in-php-applications)** あまり使ったことが無いツールなどもあり気になる ::: ::: info **[What's new in PHP 8.3](https://stitcher.io/blog/new-in-php-83)** いつもお世話になっております🙇‍♂️ ::: ::: info **[Mastering Clean Code in PHP: Principles, Best Practices, and Real-world Examples](https://mderis.medium.com/mastering-clean-code-in-php-principles-best-practices-and-real-world-examples-62a2ff550944)** いずれも重要なことなので再認識しておきたい ::: ::: info **[8 questions to the PHP memory usage](https://medium.com/@serhii.shkarupa/8-questions-to-the-php-memory-usage-e49ae105bade)** 恥ずかしながらあまり意識したことが無い部分だったので興味深かったです ::: ::: info **[The RFC Vote project](https://stitcher.io/blog/rfc-vote)** 投票権が無いユーザ側も賛成・反対の意思表示ができるのは興味深く、色んな意見が見れそうで面白そうです。 ::: ::: info **[PHPカンファレンス関西2024 開催決定]()** https://twitter.com/phpcon_kansai/status/1692491532363038781?s=20 https://twitter.com/YKanoh65/status/1692511662182650202?s=20 https://phpkansai.connpass.com/event/293259/ ::: # 特集:PHP8.3の新機能について語り合う ## [Marking overridden methods](https://wiki.php.net/rfc/marking_overriden_methods) オブジェクトを継承していることを示すアトリビュート `#[\Override]` が追加される。 * インターフェースを実装しているのか、クラスを継承してオーバーライドしているのかを明示できる * 親クラスのシグニチャが変わった場合などで意図しないオーバーライドにならないようにできる 例えば、フレームワークやライブラリのバージョンアップ時に破壊的な変更が加わった際の影響を調べやすくなる。 Java の `@Override` や TypeScript の `override` と同じ目的。 ### 使用例 ```php= class P { protected function p(): void {} } class C extends P { #[\Override] public function p(): void {} } ``` ### エラーが起きる例 ```php= class P { private function p(): void {} } class C extends P { #[\Override] public function p(): void {} // Fatal error!!! // C::p() には #[\Override] アトリビュートがあるが 親クラスにオーバーライド元メソッドがない } ``` **プロパティのオーバーライドは対象外** プロパティは親クラスと子クラスで目的が異なる場合があることが多い → 本機能で実現したいことと一致しない ## [Typed Class Constants](https://wiki.php.net/rfc/typed_class_constants) クラス、インターフェイス、トレイト、および enum の定数に型を設定できるようになる。 ```php= enum E { const string TEST = "Test1"; // E::TEST is a string } trait T { const string TEST = E::TEST; // T::TEST is a string too } interface I { const string TEST = E::TEST; // I::TEST is a string as well } class Foo implements I { use T; const string TEST = E::TEST; // Foo::TEST must also be a string } class Bar extends Foo { const string TEST = "Test2"; // Bar::TEST must also be a string, but the value can change } ``` クラス定数は共変であるため、継承しているクラス定数の型を拡張することはできない。 ```php= trait T { public const ?array E = []; } class Test { use T; private const int A = 1; public const mixed B = 1; public const int C = 1; public const Foo|Stringable|null D = null; // This is illegal since the type cannot change when T::E is redefined public const array E = []; } class Test2 extends Test { // This is legal since Test::A is private public const string A = 'a'; // This is legal since int is a subtype of mixed public const int B = 0; // This is illegal since mixed is a supertype of int public const mixed C = 0; // This is legal since Foo&Stringable is more restrictive than Foo|Stringable public const (Foo&Stringable)|null D = null; } enum E { // This is legal since constants provide a covariant context public const static A = E::Foo; case Foo; } class Foo implements Stringable { public function __toString() { return ""; } } ``` ## [mb_str_pad](https://wiki.php.net/rfc/mb_str_pad) [str_pad()](https://www.php.net/manual/ja/function.str-pad.php) のマルチバイト文字対応版。 ```php= // This will pad such that the string will become 10 bytes long. var_dump(str_pad('Français', 10, '_', STR_PAD_RIGHT)); // BAD: string(10) "Français_" var_dump(str_pad('Français', 10, '_', STR_PAD_LEFT)); // BAD: string(10) "_Français" var_dump(str_pad('Français', 10, '_', STR_PAD_BOTH)); // BAD: string(10) "Français_" // This will pad such that the string will become 10 characters long, and in this case 11 bytes. var_dump(mb_str_pad('Français', 10, '_', STR_PAD_RIGHT));// GOOD: string(11) "Français__" var_dump(mb_str_pad('Français', 10, '_', STR_PAD_LEFT)); // GOOD: string(11) "__Français" var_dump(mb_str_pad('Français', 10, '_', STR_PAD_BOTH)); // GOOD: string(11) "_Français_" ``` mbstring に新規追加される関数のため、下位互換性のない変更はない 同名の関数を自作している場合は影響がある。 ちなみに、GitHub で該当の関数名で[検索](https://github.com/search?q=mb_str_pad+lang%3Aphp&type=code)したところ 326件 ヒットした模様。 ## [Dynamic class constant fetch](https://wiki.php.net/rfc/dynamic_class_constant_fetch) クラス定数を動的に指定することができるようになる ```php= class Foo { const BAR = 'bar'; } $bar = 'BAR'; // PHP8.3 以降は以下の記述が可能 echo Foo::{$bar}; // PHP8.2 までで上記と同様の動作を実現する方法 echo constant(Foo::class . '::' . $bar); ``` 未定義のクラス定数にアクセスすると Error が発生。 ```php= class Foo {} $bar = 'BAR'; echo Foo::{$bar}; ``` 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) - これまでのstatic変数の構文では、初期化には固定値しか入れられなかった - その制限を緩めて、static変数の初期化時に変数や関数を渡せるようになる ```php= function bar() { echo "bar() called\n"; return 1; } function foo() { static $i = bar(); echo $i++, "\n"; } foo(); // bar() called // 1 foo(); // 2 foo(); // 3 ``` ## [Readonly amendments](https://wiki.php.net/rfc/readonly_amendments) - readonlyプロパティをcloneするときに再初期化することが可能になる。 ```php= // __clone()の実行中のみ、readonlyプロパティを再初期化することができる 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() { // __cloneから呼び出されている場合はreadonlyプロパティの変更がOK unset($this->baz); } } $foo = new Foo(new DateTime(), new DateTime()); $foo2 = clone $foo; // エラーは発生しない。 // この場合、Foo2::$bar は2重にcloneされており、Foo2::$baz は初期化されない ``` - しかし、__cloneメソッドでreadonlyプロパティを変更できるのは1回のみ - 以下のようにTest::$barを2回変更しようとした場合はエラーが発生する ```php= class Test { public function __construct( public readonly DateTime $bar ){} public function __clone() { $this->bar = $this->bar; // OK $this->bar = clone $this->bar; // NG } } ``` ## [PDO driver specific sub-classes](https://wiki.php.net/rfc/pdo_driver_specific_subclasses) 1. 各ドライバ固有のメソッドを持つPDOのサブクラスを追加 2. 特定のDBのサブクラスを取得するPDO::connectを追加 #### 1. 各ドライバ固有のメソッドを持つPDOのサブクラスを追加 - PDOクラスに各データベースに特化したサブクラスが追加される - データベース専用の機能がサブクラスで実行可能 ```php= // MySQL $pdoMySQL = new PdoMySql($dsn); $pdoMySQL->getWarningCount(); // MySQL専用機能 // Postgres $pdoPgsql = new PdoPgsql($dsn); $pdoMySQL->getPid(); // Postgres専用機能 ``` #### 2. 特定のサブクラスを作成できるPDO::connectを追加 - `PDO::connect`というファクトリメソッドが新たに追加 - DBの接続を行い、接続したDBの種類に合わせて特定のサブクラスのオブジェクトが返り値として渡される - なので、DSNが`PostgreSQL`に接続した場合は`PdoPogsql`が、`MySql`に接続した場合は`PdoMySql`が返却される ```php= class PDO { public static function connect(string $dsn [, string $username [, string $password [, array $options ]]]) { if (connecting to SQLite DB) { return new PdoSqlite(...); } return new PDO(...); } } ``` また、以下のように各DBのサブクラスのコンストラクタを使って直接接続することも可能 ```php= $db = new PdoSqlite($dsn, $username, $password, $options); ``` ## [Randomizer Additions](https://wiki.php.net/rfc/randomizer_additions) `Randomizerクラス`に以下の関数が追加 #### 1. `getBytesFromString()` 与えられた文字列、文字列長を参照してランダムに文字列を生成。  第一引数`$string`:選択対象の文字列  第二引数`length`:返り値の文字列長 ```php=: $randomizer = new \Random\Randomizer(); // ランダムなドメイン名 var_dump(sprintf( "%s.example.com", $randomizer->getBytesFromString('abcdefghijklmnopqrstuvwxyz0123456789', 16) )); // string(28) "xfhnr0z6ok5fdlbz.example.com" // 多要素認証の認証コード var_dump( implode('-', str_split($randomizer->getBytesFromString('0123456789', 20), 5)) ); // string(23) "09898-46592-79230-33336" // 小数 var_dump(sprintf( '0.%s', $randomizer->getBytesFromString('0123456789', 30) )); // string(30) "0.217312509790167227890877670844" ``` #### 2. `getFloat()` 引数`$min`と`$max`の間の浮動小数点数を返す。 `$min`と`$max`の区間境界を変更するには、第三引数の`$boundary`を指定する必要がある。 - 第一引数`$min`:最小値 - 第二引数`$max`:最大値 - 第三引数`$boundary`:`$min`,`$max` の区間境界を決める - `\Random\IntervalBoundary::ClosedOpen`:`[$min, $max)`(デフォルト) - `ClosedClosed`:`[$ming, $max]` - `ClosedOpen`:`($min, $max]` - `OpenOpen`:`($min, $max)` ```php= $randomizer = new \Random\Randomizer(); // 経緯度 var_dump(sprintf( "Lat: %+.6f Lng: %+.6f", $randomizer->getFloat(-90, 90, \Random\IntervalBoundary::ClosedClosed), // 緯度は90/-90どちらも可 $randomizer->getFloat(-180, 180, \Random\IntervalBoundary::OpenClosed), // 経度は180はあるけど, -180はない )); // string(32) "Lat: -51.742529 Lng: +135.396328" ``` また、これに伴い名前空間`\Random\IntervalBoundary`が予約されるので使用不可となる。 #### 3. `nextFload()` `getFloat(0, 1, \Random\IntervalBoundary::ClosedOpen)`と同等の処理を実行。 内部実装が`getFloat()`よりも単純にまとまっているので、処理速度が速いとのこと。 `[0,1)`のランダムな少数を生成する場合はこっちを使うとよい。 ```php= $randomizer = new \Random\Randomizer(); // 50%の確率 var_dump( $randomizer->nextFloat() < 0.5 ); // bool(true) // 10%の確率 var_dump( $randomizer->nextFloat() < 0.1 ); // bool(false) ``` ## [json_validate function()](https://wiki.php.net/rfc/json_validate) 文字列が正しいJSON文字列であるかを検証する関数`json_validate()`が追加される。 ``` php json_validate ( string $json 、 int $ Depth = 512 、 int $flags = 0 ) : bool ``` ### 現状の問題点 `json_decode()`や正規表現でも確認はできるが、検証以外の不要な処理が入っていてパフォーマンスに影響が有ったり、処理が複雑になったりといった問題がある。([json_validate() 関数が必要な理由](https://wiki.php.net/rfc/json_validate#reasons_to_have_json_validate_function_in_the_core)) ### 使用例 ``` php json_validate('{ "test": { "foo": "bar" } }'); // true json_validate('{ "": "": "" } }'); // false ``` ## [Define proper semantics for range() function](https://wiki.php.net/rfc/proper-range-semantics) [range()](https://www.php.net/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 ``` ## [More Appropriate Date/Time Exceptions](https://wiki.php.net/rfc/datetime-exceptions) DateTimeのエラーハンドリング機能が改善されます。 * これまでDateTimeは`Exception`、`Error`などのエラーを発生させていたが、より具体的な例外を投げるよう改善 * 警告、エラーのメッセージを改善 ※オブジェクト型のみ影響を受ける(手続き型の書き方は依然と同じ挙動) ``` 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 // 手続き型の場合は依然と同じ挙動 $sub = date_sub(new DateTime(), $i); // PHP8.2 Warning: date_sub(): Only non-special relative time specifications are supported for subtraction // PHP8.3 Warning: date_sub(): Only non-special relative time specifications are supported for subtraction ``` ## [Improve unserialize() error handling](https://wiki.php.net/rfc/improve_unserialize_error_handling) [unserialize](https://www.php.net/unserialize)関数のエラーハンドリング機能が強化のための準備が行われます。 これまで様々なエラーや例外を発生させいましたが、PHP9からは`UnserializationFailedException`で処理できるようになる予定で、その準備が実施されます。 ``` 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 ``` ## [Make unserialize() emit a warning for trailing data](https://wiki.php.net/rfc/unserialize_warn_on_trailing_data) [unserialize](https://www.php.net/unserialize)関数について。 unserialize関数の末尾に不要なバイトがある場合は警告を表示するようになります。 ``` php <?php var_dump(unserialize('i:5;i:6;')); // Warning: unserialize(): Extra data starting at offset 4 of 8 bytes in %s on line %d // int(5) var_dump(unserialize('N;i:6;')); // Warning: unserialize(): Extra data starting at offset 2 of 6 bytes in %s on line %d // NULL var_dump(unserialize('b:1;i:6;')); // Warning: unserialize(): Extra data starting at offset 4 of 8 bytes in %s on line %d // bool(true) var_dump(unserialize('a:1:{s:3:"foo";b:1;}i:6;')); // Warning: unserialize(): Extra data starting at offset 20 of 24 bytes in %s on line %d // array(1) { // ["foo"]=> // bool(true) // } ``` ## [Saner array_(sum|product)()](https://wiki.php.net/rfc/saner-array-sum-product) array_sum関数とarray_product関数の改善。 array_sum関数とarray_product関数の動作 - entryのtypeがarrayもしくはobjectの時はそのentryをスキップする。 - それ以外の場合、entryは数値(int/float)にキャストされる - boolはtrueが1、falseは0、nullは0... 現在の動作 ``` php /* STDERR gets cast to 3 */ $input = [true, STDERR, new stdClass(), [], gmp_init(6)]; $output = array_sum($input); var_dump($output); // int(4) $output = array_product($input); var_dump($output); // int(3) ``` 8.3以降 キャスト可能なオブジェクトはキャストされる。 ``` php /* gmp_init(6) は 6 にキャストされます */ $input = [true, STDERR, new stdClass(), [], gmp_init(6)]; $output = array_sum($input); var_dump($output); /* Warning: array_sum(): Addition is not supported on type resource in %s on line %d Warning: array_sum(): Addition is not supported on type stdClass in %s on line %d Warning: array_sum(): Addition is not supported on type array in %s on line %d int(10) */ $output = array_product($input); var_dump($output); /* Warning: array_product(): Multiplication is not supported on type resource in %s on line %d Warning: array_product(): Multiplication is not supported on type stdClass in %s on line %d Warning: array_product(): Multiplication is not supported on type array in %s on line %d int(18) */ ``` ## [Path to Saner Increment/Decrement operators](https://wiki.php.net/rfc/saner-inc-dec-operators) phpの算術演算子はint,float以外かなり複雑です。 例えば、bool型の場合は何も動作しませんが、string型の場合はキャストされたのちに処理が実行されます。 ``` php $int = 10 ; var_dump ( ++ $int ) ; // int(11) $int = 10 ; var_dump ( - $int ) ; // int(9) $float = 5.7 ; var_dump ( ++ $float ) ; // float(6.7) $float = 5.7 ; var_dump ( - $float ) ; // float(4.7) $false = false ; var_dump ( ++ $false ) ; // bool(false) var_dump ( -- $false ) ; // bool(false) $true = true ; var_dump ( ++ $true ) ; // bool(true) var_dump ( -- $true ) ; // bool(true) $stringInt = "10" ; var_dump ( ++ $stringInt ) ; // int(11) var_dump ( -- $stringInt ) ; // int(9) $stringFloat = "5.7" ; var_dump ( ++ $stringFloat ) ; // float(6.7) var_dump ( -- $stringFloat ) ; // float(4.7) ``` また、nullに対する処理は何も実行されません。 空文字をインクリメントすると文字列"1"になります。 ``` php $n = null; ++$n; var_dump($n); // int(1) $s1 = $s2 = ""; var_dump(++$s1, ++$s1, --$s2, --$s2); /* this results in string(1) "1" int(2) int(-1) int(-2) */ ``` 8.3以降では、以下のようになります。 - nullに対するデクリメント → 警告 + nullのまま ``` php $n = null; --$n; // Warning: Decrement on type null has no effect, this will change in the next major version of PHP var_dump($n); // NULL ``` - falseに対するインクリメント/デクリメント - → 警告 + falseのまま ``` php $false = false; --$false; // Warning: Decrement on type bool has no effect, this will change in the next major version of PHP var_dump($false); // bool(false) ++$false; // Warning: Increment on type bool has no effect, this will change in the next major version of PHP var_dump($false); // bool(false) ``` - trueに対するインクリメント/デクリメント - → 警告 + falseのまま ``` php $true = true; --$true; // Warning: Decrement on type bool has no effect, this will change in the next major version of PHP var_dump($true); // bool(true) ++$true; // Warning: Increment on type bool has no effect, this will change in the next major version of PHP var_dump($true); // bool(true) ``` - 数値以外の文字列でのデクリメント演算子 → 非推奨 ``` php $empty = ""; --$empty // Deprecated: Decrement on empty string is deprecated as non-numeric var_dump($empty); // int(-1) $s = "foo"; --$s; // Deprecated: Decrement on non-numeric string has no effect and is deprecated var_dump($s); // string(3) "foo" ``` 英数字ではない文字列に対するインクリメントも非推奨になります。 文字列に対するインクリメントは`str_increment`、デクリメント処理は`str_decrement `を仕様することになります。 ## [Use exceptions by default in SQLite3 extension](https://wiki.php.net/rfc/sqlite3_exceptions) * SQLite3拡張モジュールの修正 * SQLite3ではエラー時に例外をスローするかどうかを設定できる * PHP8.3以前ではデフォルトで「例外をスローしない」だった * PHP8.3以降はデフォルトで「ストーする」になる なお、今後は「エラー時に例外をスローするかどうか」を設定する機能はなくなり、かならzうスローする方向で修正するとのこと ## [Deprecate remains of string evaluated code assertions](https://wiki.php.net/rfc/assert-string-eval-cleanup) * `assert_options()`が非推奨になります ## Deprecate functions with overloaded signatures * 標準関数に用意されている「オーバーロード用のシグネチャ」を複数廃止する * 削除されたものについては代替が用意される予定 * PHPコアのリファクタリング? ## Deprecations for PHP 8.3 ### mb_strimwidth() * 指定した幅で文字列を丸める関数 * 負の値を与えられなれなくなる ### 定数 NumberFormatter::TYPE_CURRENCY の削除 * numfmt_create() で使用するもので、フォーマッタの形式を指定する定数 * 通貨値としてフォーマット/パースすることを指定するものだった * 実際は実装されなかったので削除 ### 定数MT_RAND_PHP削除 * PHP7.1で修正された乱数発生機問題の「互換性維持」手段 * あえて壊れた乱数を発生させるときに使われていた * 今回で廃止