【PHP8.3】 クラス定数とENUMを文字列から探せるようになる

PHPの[ENUM](https://www.php.net/manual/ja/language.enumerations.php)にはひとつ重大な欠点がありまして、キーを文字列指定することができません。

どういうこと？

```php
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()](https://www.php.net/manual/ja/backedenum.from.php)はありますが、これは値で検索するというものであり、完全に逆の機能です。

```php
$str = 'クラブ';
echo Suit::from($str)->name; // Clubs
```

逆向きの`BackedEnum::fromName()`的なメソッドは何故か存在しません。
そんなわけで、これまではこの機能を実現するためにキーと値を逆にするというノウハウが流行していました。

```php:これは動く
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](https://wiki.php.net/rfc/dynamic_class_constant_fetch)の紹介です。


# Dynamic class constant fetch

## Proposal

PHPでは、メンバー名を調べる方法が多数存在します。

```php
$$foo;          // 変数
$foo->$bar;     // プロパティ
Foo::${$bar};   // 静的プロパティ
$foo->{$bar}(); // メソッド
Foo::{$bar}();  // 静的メソッド
$foo::$bar;     // インスタンスの静的プロパティ
$foo::bar();    // インスタンスの静的メソッド
```

しかしこれには例外があり、それがクラス定数です。

```php
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されます。
これは普通のクラス定数アクセスと同じ動作です。

```php
class Foo {}

$bar = 'BAR';
echo Foo::{$bar}; // Error: Undefined constant Foo::BAR
```

### {} expression type

中括弧の式の結果は文字列でなければなりません。
それ以外の場合はTypeErrorがthrowされます。

```php
echo Foo::{[]};
// TypeError: Cannot use value of type array as class constant name
```

### Order of execution

残念なことに、ルックアップの実行順はかなり直感に従いません。

```php
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
```


プロパティや配列へのアクセスは、実際に操作を実行するより前に内部の式を評価します。
これは[かなりテクニカルな理由](https://www.npopov.com/2017/04/14/PHP-7-Virtual-machine.html#writes-and-memory-safety)によるものです。

プロパティや配列にアクセスしている間は、基本的にユーザコードを実行するべきではありません。
ポインタの再割り当てが起こり、ポインタが無効になってしまう可能性があるためです。

しかしこの問題は、クラス定数には当てはまりません。
そのため、よりシンプルで直感的なアプローチが採用されます。

```php
Foo::{test('foo')}::{test('bar')};
// foo
// Class constant foo
// bar
// Class constant bar
```

### Magic 'class' constant

マジック定数[class](https://www.php.net/manual/ja/language.constants.magic.php)も動的アクセスが可能になります。

```php
namespace Foo;
 
$class = 'class';
echo Bar::{$class}; // Foo\Bar
```

### Enums

この機能は、ENUMでも同様に動作します。

## Future scope

この項目は将来の展望であり、このRFCには含まれません。

### Interaction with ??

このRFCは、[NULL合体演算子](https://www.php.net/manual/ja/language.operators.comparison.php#language.operators.comparison.coalesce)`??`の既存動作に影響を与えません。
すなわち、`Foo::{$bar} ?? null;`は、該当の定数が存在しなければErrorをthrowします。
他のタイプのメンバーアクセス同様、このエラーを抑制することはできますが、そのような挙動であるべきかはわかりません。

## Vote

投票者の2/3の賛成で受理されます。

投票期間は2022/12/22から2023/01/05です。
本RFCは、賛成15反対4の賛成多数で可決されました。



# 感想

このRFC自体はENUMではなくクラス定数を目的としたものなのですが、その[元となったIssue](https://github.com/php/php-src/issues/9352)はENUMに対するものです。
そしてENUMはクラスを魔改造して作られたものです。

ということで、クラス定数を動的指定可能にすることでENUMも動的指定が可能になりました。
ENUMの使用がますます便利になりますね。