RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

PHPerのための「PHP8.3の新機能」を語り合う【PHP TechCafe イベントレポート】

 弊社で毎月開催し、PHPエンジニアの間で好評いただいているPHP TechCafe。2023年8月のイベントでは「PHP8.3の新機能」について語り合いました。弊社のメンバーが事前にまとめてきた情報にしたがって、他の参加者に意見を頂いて語り合いながら学びました。今回はその内容についてレポートします。

rakus.connpass.com

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というファクトリメソッドが追加されます。

$dsnPostgreSQLに接続したら、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:可能な限りもっとも短いソースコードで記述することを競う

Copyright © RAKUS Co., Ltd. All rights reserved.