弊社で毎月開催し、 PHP エンジニアの間で好評いただいている PHP TechCafe。2023年8月のイベントでは「PHP8.3の新機能」について語り合いました。弊社のメンバーが事前にまとめてきた情報にしたがって、他の参加者に意見を頂いて語り合いながら学びました。今回はその内容についてレポートします。 rakus.connpass.com PHP8.3 新機能について Marking overridden method オブジェクトを継承していることを示すattributeが追加 ※プロパティのオーバーライドは対象外 Type Class Constants class、interface、trait、およびenumの定数に型を設定できるようになった ※継承しているクラス定数の型を拡張することはできない。 mb_str_pad str_padのマルチバイト用関数が追加 Dynamic class constant fetch クラス定数を動的に指定することができるようになった Arbitrary static variable initializers static変数の初期化時に、固定値以外の変数や関数を渡せるようになった Readonly amendment readonlyプロパティをcloneするとき再初期化することが可能になった ※cloneメソッドでreadonlyを変更できるのは一回のみ PDO driver specific sub-classes PDOのサブクラスを追加 PDO::connect Randomizer Admditions Randomizerクラスに以下の関数が追加 getBytesFromString() getFloat() nextFloat() その他の関数追加修正 json_validate() range() mb_strimwidth() その他議題 定数NumberFormatter::TYPE_CURRENCYの削除 定数MT_RAND_PHPの削除 最後に PHP8.3 新機能について PHP8.3の新機能は弊社のメンバーが事前にHackMdの記事としてまとめています。 今回のイベントではこの記事に沿って新機能をみていきました。 hackmd.io すべての機能は今回確認することはできなかった為、 確認する場合はこちらの記事にてよろしくお願いいたします。 Marking overridden method オブジェクトを継承していることを示すattributeが追加 <?php class P { protected function p () : void {} } class C extends P { #[\Override] public function p () : void {} } このことにより、 ・インターフェースを実装してるのか、クラスを継承してオーバーライドしているのか明示 ・親クラスの シグニチャ が変わった場合で、意図しないオーバライドを防ぐ ができます。 メリット、用途が分かりやすく今回の新機能の中で一番注目が集まった機能です。 アトリビュート が追加されたのはPHP8.0からなので、その アトリビュート 機能を使った活用が早いとして 喜々として話されておりました。 コメントからは、「解析ツールを使って自動で付けて回ることが出来そう」と。 可読性、保守性の向上が期待できそうです。 ※プロパティのオーバーライドは対象外 プロパティのオーバーライドは、親クラスと子クラスで意味合いが変わることが多いので、 対象外となります。 Type Class Constants class、interface、trait、および enum の定数に型を設定できるようになった <?php enum E { const string TEST = "Test1" ; // string 型の指定が可能になりました } この議題では、 PHP で今まで出来ていなかったことに驚くかたもおられました。 ※継承しているクラス定数の型を拡張することはできない。 以下コード例で言いますと、 public const mixed C = 0; の部分です。 これは、int ⇒ mixed になっているためできません。 少しややこしいですが、 定義の範囲が広くなったらNG と認識して問題ないです。 コメントでは、子クラスで型を再定義するようなケースに突っ込みがありました。 なるべく避けましょう。 <?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 ; // T::Eが再定義されたときに型を変更することはできないのでNG。 public const array E = [] ; } class Test2 extends Test { // private ⇒ public は 任意に型変更 OK public const string A = 'a' ; // mixed ⇒ int は OK public const int B = 0 ; // int ⇒ mixed は NG public const mixed C = 0 ; // since Foo&Stringable ⇒ Foo|Stringable は OK public const ( Foo & Stringable ) | null D = null ; } enum E { // 定数は共変のコンテキストを提供するのでOK public const static A = E :: Foo; case Foo; } class Foo implements Stringable { public function __toString () { return "" ; } } mb_str_pad str_padのマルチバイト用関数が追加 <?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_" こちらの議題では、追加された内容よりも、 マルチバイトと PHP の話にスポットが当たっておりました。 というのも、マルチバイト文字と言えば、日本語のひらがな、カタカナ、そして漢字。 国内で苦しめられているエンジニアも少なくありません。 (議題でもマルチバイトの闇がぽつりと溢れだす場面が) しかし世界で見れば需要は少ないのか、 RFC では蔑ろにされるイメージがあり、 今回の追加は珍しいとの反応が。 RFC を確認すると著者はフランスの方で、 例として、フランス語、 ギリシャ 語、絵文字が記載されております。 欧州ではISO8859-1からISO8859-16まであり、 地域によって切替を行う方式なので、追加したくなったのかなと推測されておりました。 Dynamic class constant fetch クラス定数を動的に指定することができるようになった <?php class Foo { const BAR = 'bar' ; } $ bar = 'BAR' ; // PHP8.3 以降は以下の記述が可能 echo Foo :: { $ bar } ; // PHP8.2 までで上記と同様の動作を実現する方法 echo constant ( Foo :: class . '::' . $ bar ) ; 上記コードを一目見て、直感的な理解の難しさ故に「黒魔術に見える」との意見がありました。 echo Foo::{$bar}; ポイントはこの部分で、 文字列"BAR"が格納されている$barを使用し、Foo関数のBAR(文字列bar)を呼び出しております。 enum の使い勝手が向上することは良いですね。 Arbitrary static variable initializers static変数の初期化時に、固定値以外の変数や関数を渡せるようになった <?php function bar () { echo "bar() called \n " ; return 1 ; } function foo () { static $ i = bar () ; // ← 8.2まではこの書き方が出来なかった echo $ i ++ , " \n " ; } foo () ; // bar() called // 1 foo () ; // 2 foo () ; // 3 この議題で良い活用法について思案しており、 コメントの中で「キャッシュの初期値設定は楽になりそう」との意見がありました。 本題とは関係ありませんが、このように皆で活用法を考えて、 共有することができるのは、 PHP TechCafeの良いところですね。 Readonly amendment readonlyプロパティをcloneするとき再初期化することが可能になった cloneは インスタンス をコピーする関数です。 clone時に、readonlyのプロパティを一度だけ変更することが可能になります。 下記コード例で言いますと、 $this->bar = clone $this->bar が新機能にあたります。 <?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を変更できるのは一回のみ 一回目は変更できるが、二回目はNGになります。 <?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 PDOのサブクラスを追加 各ドライバ固有のメソッドを持つPDO( PHP Data Objects)のサブクラスが追加されます。 <?php // MySQL $ pdoMySQL = new PdoMySql ( $ dsn ) ; $ pdoMySQL -> getWarningCount () ; // MySQL専用機能 // PostgreSQL $ pdoPgsql = new PdoPgsql ( $ dsn ) ; $ pdoMySQL -> getPid () ; // PostgreSQL専用機能 例として、 PostgreSQL にあるpidの取得が専用の関数により簡単になります。 専用の関数なので、これらの関数を使用した際はDB移行時に注意してください。 PDO::connect 特定のDBのサブクラスを取得する PDO::connect というファクトリメソッドが追加されます。 $dsn が PostgreSQL に接続したら、 PostgreSQL のPDO、 MySQL に接続したら MySQL のPDOが返却されます。 <?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 = new PdoSqlite($dsn, $username, $password, $options); 議題として、何でもOKなファクトリメソッドを用意しているが、 専用のコンスト ラク タを用意されていると、そちらを使うのがメインになると思うのではとの意見がでました。 Randomizer Admditions Randomizerクラスに以下の関数が追加 getBytesFromString() 与えられた文字列、文字列長を参照してランダムに文字列を生成。 第一引数 $string :選択対象の文字列 第二引数 $length :返り値の文字列長 下記例では、半角小文字英数字の中から16桁のランダムな文字列を生成します。 <?php $ randomizer = new \Random\Randomizer () ; // ランダムなドメイン名 var_dump ( sprintf ( "%s.example.com" , $ randomizer -> getBytesFromString ( 'abcdefghijklmnopqrstuvwxyz0123456789' , 16 ) )) ; // string(28) "xfhnr0z6ok5fdlbz.example.com" こちらに対して、第一引数に 正規表現 は使用不可とのことで、惜しむ声が少しありました。 これからの改善に注目です。 getFloat() 引数$minと$maxの間の 浮動小数点数 を返す。 第一引数 $min :最小値 第二引数 $max :最大値 第三引数 $boundary : 区間 境界の指定 ※デフォルトは ClosedOpen \Random\IntervalBoundary::ClosedOpen : $min以上、 $maxより下 \Random\IntervalBoundary::ClosedClosed : $min以上、 $max以下 \Random\IntervalBoundary::OpenClosed : $minより上、 $max以下 \Random\IntervalBoundary::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" こちらの用途については、次の引数の紹介も併せます。 nextFloat() 0~1の間でランダムな少数を出してくれます。 以下コードと同等の処理を実行。 getFloat(0, 1, \Random\IntervalBoundary::ClosedOpen) 内部実装がgetFloat()よりも整理されており、処理速度が速いとのこと。 0.0以上、 1.0より下 のランダムな少数を生成する場合はこっちを使うとよい。 この機能を作られた方はゲーム会社に勤めているとのことで納得の声があがりました。 業務アプリを作成する場合は乱数に頼る機会は少ないがゼロではない為、 こういった数値機能が豊富になると有り難いですね。 その他の関数追加修正 json_validate() 文字列が JSON の正しい形かどうかを判定します。 <?php json_validate ( '{ "test": { "foo": "bar" } }' ) ; // true json_validate ( '{ "": "": "" } }' ) ; // false 今まで、 JSON 形式のチェックは json_decode によるチェックを行っていたかと思いますが、 JSON の大きさによって大量のメモリを割り当てる必要がありました。 この関数を使用するとその心配がなくなります。 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 こちら、誤った指定でエラーになるようになります。 var_dump(range('9', 'A')); は、 変更後も挙動が分かり難いですが、ASCIIコードの順になります。 現在Range関数を使用している場合は、不自然な挙動を前提に動いている可能性がないか注意が必要です。 コメントでは、 コードゴルフ *1 の選択肢が減りますね!とのことで笑いが起きました。 mb_strimwidth() 指定した幅で文字列を丸める関数ですが、負の値を入れることが出来てしまった問題が修正されます。 使用されている場合は、負の値が今まで入っていたが動いていた場合があるので注意が必要です。 その他議題 定数NumberFormatter::TYPE_CURRENCYの削除 フォーマッタの形式を指定する定数です。 通貨の値をフォーマッタ化する定数がありましたが、実装されないままだったので消すことになりました。 定数MT_RAND_ PHP の削除 PHP7.1で修正された乱数発生機問題の「互換性維持」手段が、今回でなくなることになります。 最後に PHP TechCafeでは PHP の機能をなぞるだけではなく、 新機能の活用法について皆で考えたり、 有識者 に機能が追加された背景まで語りつくしていただけました! そのため、すべての変更点をなぞることができませんでしが、 ある意味 PHP TechCafeの気軽さならではかと思います笑 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております。 *1 : 可能な限りもっとも短い ソースコード で記述することを競う