PHPのENUMにはひとつ重大な欠点がありまして、キーを文字列指定することができません。
どういうこと?
enum Suit:string{
case Hearts = 'ハート';
case Diamonds = 'ダイヤ';
case Clubs = 'クラブ';
case Spades = 'スペード';
}
echo Suit::Hearts->value; // ハート
$str = 'Diamonds'; // このキーから値を取りたい
echo Suit::{$str}->value; // syntax error
この制限のせいで、たとえばHTTPリクエストで送られてきた値からENUMを取り出すといったことができませんでした。
それっぽいメソッドBackedEnum::from()はありますが、これは値で検索するというものであり、完全に逆の機能です。
$str = 'クラブ';
echo Suit::from($str)->name; // Clubs
逆向きのBackedEnum::fromName()
的なメソッドは何故か存在しません。
そんなわけで、これまではこの機能を実現するためにキーと値を逆にするというノウハウが流行していました。
enum Suit:string{
case ハート = 'Hearts';
case ダイヤ = 'Diamonds';
case クラブ = 'Clubs';
case スペード = 'Spades';
}
echo Suit::ハート->value; // Hearts
これはもちろん冗談ですが。
ということで、これをどうにかするRFCが提出され受理されました。
PHP8.3からは、冒頭のSuit::{$str}
が動作するようになります。
以下は該当のRFC、Dynamic class constant fetchの紹介です。
Dynamic class constant fetch
Proposal
PHPでは、メンバー名を調べる方法が多数存在します。
$$foo; // 変数
$foo->$bar; // プロパティ
Foo::${$bar}; // 静的プロパティ
$foo->{$bar}(); // メソッド
Foo::{$bar}(); // 静的メソッド
$foo::$bar; // インスタンスの静的プロパティ
$foo::bar(); // インスタンスの静的メソッド
しかしこれには例外があり、それがクラス定数です。
class Foo {
const BAR = 'bar';
}
$bar = 'BAR';
echo Foo::{$bar}; // syntax error
echo constant(Foo::class . '::' . $bar); // こっちは動く
この制限に意味があるものとは思えません。
このRFCでは、クラス定数に上記のような構文を取り入れることを提案します。
Semantics
Non-existent class constants
存在しないクラス定数に名前でアクセスするとErrorがthrowされます。
これは普通のクラス定数アクセスと同じ動作です。
class Foo {}
$bar = 'BAR';
echo Foo::{$bar}; // Error: Undefined constant Foo::BAR
{} expression type
中括弧の式の結果は文字列でなければなりません。
それ以外の場合はTypeErrorがthrowされます。
echo Foo::{[]};
// TypeError: Cannot use value of type array as class constant name
Order of execution
残念なことに、ルックアップの実行順はかなり直感に従いません。
function test($value) {
echo $value . "\n";
return $value;
}
class Foo implements ArrayAccess {
public function __get($property) {
echo 'Property ' . $property . "\n";
return $this;
}
public function __call($method, $arguments) {
echo 'Method ' . $method . "\n";
return $this;
}
public static function __callStatic($method, $arguments) {
echo 'Static method ' . $method . "\n";
return static::class;
}
public function offsetGet($offset): mixed {
echo 'Offset ' . $offset . "\n";
return $this;
}
public function offsetExists($offset): bool {}
public function offsetSet($offset, $value): void {}
public function offsetUnset($offset): void {}
}
$foo = new Foo();
$foo->{test('foo')}->{test('bar')};
// foo
// bar
// Property foo
// Property bar
$foo->{test('foo')}()->{test('bar')}();
// foo
// Method foo
// bar
// Method bar
Foo::{test('foo')}()::{test('bar')}();
// foo
// Static method foo
// bar
// Static method bar
$foo[test('foo')][test('bar')];
// foo
// bar
// Offset foo
// Offset bar
// __getStaticは今のところないので動かない
Foo::${test('foo')}::${test('bar')};
// foo
// Static property foo
// bar
// Static property bar
プロパティや配列へのアクセスは、実際に操作を実行するより前に内部の式を評価します。
これはかなりテクニカルな理由によるものです。
プロパティや配列にアクセスしている間は、基本的にユーザコードを実行するべきではありません。
ポインタの再割り当てが起こり、ポインタが無効になってしまう可能性があるためです。
しかしこの問題は、クラス定数には当てはまりません。
そのため、よりシンプルで直感的なアプローチが採用されます。
Foo::{test('foo')}::{test('bar')};
// foo
// Class constant foo
// bar
// Class constant bar
Magic 'class' constant
マジック定数classも動的アクセスが可能になります。
namespace Foo;
$class = 'class';
echo Bar::{$class}; // Foo\Bar
Enums
この機能は、ENUMでも同様に動作します。
Future scope
この項目は将来の展望であり、このRFCには含まれません。
Interaction with ??
このRFCは、NULL合体演算子??
の既存動作に影響を与えません。
すなわち、Foo::{$bar} ?? null;
は、該当の定数が存在しなければErrorをthrowします。
他のタイプのメンバーアクセス同様、このエラーを抑制することはできますが、そのような挙動であるべきかはわかりません。
Vote
投票者の2/3の賛成で受理されます。
投票期間は2022/12/22から2023/01/05です。
本RFCは、賛成15反対4の賛成多数で可決されました。
感想
このRFC自体はENUMではなくクラス定数を目的としたものなのですが、その元となったIssueはENUMに対するものです。
そしてENUMはクラスを魔改造して作られたものです。
ということで、クラス定数を動的指定可能にすることでENUMも動的指定が可能になりました。
ENUMの使用がますます便利になりますね。