クラス定義での機能強化
まずは、クラス定義における機能強化について紹介します。
プロパティフック
PHP 8.4では、プロパティフックにより、プロパティに直接ゲッタ/セッタを記述できるようになりました。以前のPHPでは、プロパティのゲッタ/セッタの提供は以下の手段をとっていました。
- getXxxx、setXxxxというメソッドを設ける
- __get、__setといったマジックメソッドを使う
- __constructorメソッド(コンストラクタプロパティプロモーション)で簡略化したゲッタ/セッタを使う(PHP 8.0から)
getXxxx、setXxxxは単なるメソッドであるため、PHPのバージョンを問わず利用できますが、クラス定義がメソッドだらけになり、どのプロパティと対応しているのか分かりにくいといった問題があります。__get、__setではゲッタ/セッタが1箇所にまとまるので一見便利そうに見えますが、メソッド内がswitch文の分岐だらけになったり、漏れが発生したり、構造が複雑になりがちなので静的チェックがやりにくい問題があります。__constructorメソッドによる方法では、そもそもゲッタ/セッタの動作をカスタマイズする余地がありません。
そこでPHP 8.4では、プロパティフックという構文で、プロパティへのアクセスをgetフックとsetフックが横取りするような記述が可能になっています。以下のリストでは、プロパティsoupにsetフックを定義して、適正値のチェックとプロパティへのセットを実行しています。マジックメソッドを使う方法に比較して、プロパティ定義に含めて記述できるので、関連が明確です。
class Ramen
{
public string $soup {
set(string $value) {
if (!in_array($value, array('醤油', '味噌', '塩', '豚骨')))
throw new InvalidArgumentException('サポートしていない味です');
$this->soup = $value;
}
}
public string $noodle {
set(string $value) {
if (!in_array($value, array('太', '細', '縮れ')))
throw new InvalidArgumentException('サポートしていない麺です');
$this->noodle = $value;
}
}
}
$ramen = new Ramen();
$ramen->soup = '塩'; // OK
$ramen->noodle = '細'; // OK
$ramen->soup = '辛味噌'; // 例外
$ramen->noodle = 'バリカタ'; // 例外
仮想プロパティという、値を保持しないプロパティも定義可能です。プロパティの値を加工して返却する場合などに有用です。
public string $soupTaste {
get => $this->soup . '味';
}
public string $noodleType {
get => $this->noodle . '麺';
}
…略…
printf("ご注文は%sで%sです。\n", $ramen->soupTaste, $ramen->noodleType);
// ご注文は塩味で細麺です。
インタフェース内にプロパティフックの存在だけを記述することもできます。
interface Men {
public string $name { get; set; }
}
非対称可視性
PHP 8.4では、ゲッタとセッタに異なる可視性を指定できるようになりました。プロパティの参照時にはpublicでも、設定時にはprivateまたはprotectedになるといった使い分けができます。従来のPHPでは、ゲッタとセッタの可視性は共通でした。
例えば、以下のリストでは、soupプロパティはpublicですが、書き換えのみprivateに指定されているので、外部からの変更はできません。
class Ramen
{
public private(set) string $soup = '塩豚骨'; // 塩豚骨固定
}
$ramen = new Ramen();
$ramen->soup = '塩'; // セッタはprivateなのでエラー
このとき、soupプロパティのpublicは省略できます。コンストラクタプロパティプロモーションによる記述でも、非対称可視性の指定が可能です。
class Udon
{
public function __construct(
public private(set) string $tsuyu,
public private(set) string $men
) {}
}
遅延初期化
PHP 8.4では、インスタンス化をnew時点では実行せず、必要になった時点で実行する遅延初期化がサポートされました。一般的にインスタンス生成は時間のかかる処理となることがあるので、これをできるだけ後回しにしたいという考え方です。専用の実装が必要になるので、ライブラリの開発者などに向けた機能と言えます。
// 時間のかかる処理を含むクラス
class Example {
public function __construct(public int $data) {
printf("時間のかかる処理%d...\n", $data);
}
}
// イニシャライザは初期化を実行する静的関数
$initializer = static function (Example $ghost): void {
$ghost->__construct(12345);
};
// レイジーゴーストオブジェクトを作成する
$reflector = new ReflectionClass(Example::class);
$lazyObject = $reflector->newLazyGhost($initializer);
// ここではインスタンス生成はトリガーされない
var_dump($lazyObject);
// dataプロパティへのアクセスでインスタンス生成がトリガーされる
var_dump($lazyObject->data);
遅延初期化は、対象クラスの他に、インスタンスを実際に生成するイニシャライザ関数と、イニシャライザ関数を受け取って必要になった時点で呼び出すレイジーゴーストにより構成されます。
レイジーゴーストとは、遅延初期化の一つの方法で、対象クラスのオブジェクトに透過的にアクセスできる仕組みを提供し、プロパティへの最初の参照や変更でイニシャライザ関数を呼び出すことで、遅延初期化を達成します。レイジーゴーストは、クラスの情報を取り扱うReflectionClassクラスにPHP 8.4で新設された、newLazyGhostメソッドにイニシャライザ関数を渡すことで生成します。
実行結果は以下のようになります。最初のvar_dumpの実行では、インスタンスは生成されずメッセージは出力されません。次のvar_dumpでdataプロパティへのアクセスが発生するので、ここでイニシャライザが呼ばれ、インスタンスが初期化されることでメッセージが出力されます。
lazy ghost object(Example)#3 (0) { // 最初のvar_dump
["data"]=>
uninitialized(int)
}
時間のかかる処理12345... // 初期化実行
int(12345) // 2番目のvar_dump
