PHP8.2までの環境で簡単にEnumからtryNameFromしたいよね

`PHP8.3`までは動的かつ簡単に`enum`から`tryNameFrom`する事は出来ません。
と言う事でお手軽に`tryNameFrom`しようじゃあないか。

なおオチとしては`composer require putraits/enums`する方が早いです。

https://packagist.org/packages/putraits/enums

詳細な解説は[こちら](#putraitsenumsを用いた解決)。

:::note warn
注意

次の例の通りPHP8.3からは動的なenumアクセスが出来るのでこの記事は不要となります。

```php
enum HogeBackedEnum: string
{
    case Fuga = 'ふが';
}

enum HogeEnum
{
    case Fuga;
}

$name = 'Fuga';

var_dump(
    HogeBackedEnum::{$name},    // enum(HogeBackedEnum::Fuga)
    HogeEnum::{$name},          // enum(HogeEnum::Fuga)
);
```
実行例：[PHP Playground](https://php-play.dev/?c=DwfgDgFmBQ0KYDsCuBbABACQPYHM4CEBDAYwGs4ATAUWRQC40BnAFwCcBLBHaAb2jQFpihRnDQAxJDkJoAvGgDkgVwZAMgwKA3NAC%2BsRKky44NVL36DhoiVMKad0ACQJCKMfIWTpG2ADdCrAPoUqGAAFGYC2HhEZJTG9HQ8js5wWgA0gmgA9JloeighkQQk5NS0dHQehACU4QZ4ceWJTi5pGW1ZOXkFhg0V1jVVmkA&v=8.3&f=html)
:::

:::note warn
注意

本記事での実装では指定されたクラスパスがenumかどうかの判定を行っていません。
異常系に倒れた場合により多くの人が簡単に原因を特定できるようにするためです。

本来、定石としては[`enum_exists`](https://www.php.net/manual/ja/function.enum-exists.php)関数を用いて与えられたクラスパスの実在判定を行うべきです。

ですが、第二引数が`true`、つまりオートロード可能でかつ、第一引数に攻撃コードを含む文字列を与えられた場合に、意図しないクラスファイルの読み込みを阻止する事ができません。

また、第二引数を`false`にしてオートロードを抑止した場合、与えられたクラスパスが未読み込みのenumクラスパスだった場合、必ず`存在しない`扱いになるためです。
:::

## キャッシャーの準備

列挙型ではプロパティを持つことが出来ません。
そのため、動的なキャッシュを持つことが出来ません。

ならば外に出してしまえばいい。

```php:EnumNameValueMapCache.php
/**
 * 列挙型の名前 => 列挙型変換支援キャッシュクラス
 */
final class EnumNameValueMapCache
{
    /**
     * @var array 列挙型の名前 => 列挙型変換マップ
     */
    private static array $keyValueMapCache = [];

    /**
     * 指定した列挙型がキャッシュマップにあるかどうかを返します。
     *
     * @param string $enum 列挙型
     * @return bool 指定した列挙型がキャッシュマップにあるかどうか
     */
    public static function hasEnum(string $enum): bool
    {
        return \array_key_exists($enum, self::$keyValueMapCache);
    }

    /**
     * 指定した列挙型を名前 => 列挙型変換マップに登録します。
     *
     * @param object $enum 列挙型
     * @return string このクラスパス
     */
    public static function set(string $enum): string
    {
        foreach ($enum::cases() as $enum) {
            self::$keyValueMapCache[$enum::class][$enum->name]    = $enum;
        }        

        return self::class;
    }

    /**
     * 指定した列挙型で指定した名前があるかどうかを返します。
     *
     * @param string $enum 列挙型
     * @param string $name 名前
     * @return bool 指定した列挙型で指定した名前があるかどうか
     */
    public static function has(string $enum, string $name): bool
    {
        if (!\array_key_exists($enum, static::$keyValueMapCache)) {
            self::set($enum);
        }

        return isset(self::$keyValueMapCache[$enum][$name]);
    }
    
    /**
     * 指定した列挙型で指定した名前が合致するものを返します。
     *
     * @param string $enum 列挙型
     * @param strign $name 名前
     * @return null|object 列挙型
     */
    public static function get(string $enum, string $name): ?object
    {
        if (!\array_key_exists($enum, static::$keyValueMapCache)) {
            self::set($enum);
        }

        return self::$keyValueMapCache[$enum][$name] ?? null;
    }
}
```

## interface、traitの準備

当然ですが複数のenumで共通的に扱いたい訳です。
ということでinterfaceとtraitを準備します。

traitだけでも問題なく動きます。お好みや現場の状況に合わせてどうぞ。

```php:NameFromTrait.php
/**
 * 列挙型向けtryNameFrom特性
 */
trait NameFromTrait
{   
    /**
     * 名前を列挙型にマップします。
     *
     * @param string $name 名前
     * @return null|object 列挙型
     */    
    public static function tryNameFrom(string $name): ?self
    {
        if (!EnumNameValueMapCache::hasEnum(self::class)) {
            EnumNameValueMapCache::set(self::class);
        }

        return EnumNameValueMapCache::get(self::class, $name);
    }
}
```

```php:NameFromInterface.php
/**
 * 列挙型向けtryNameFromインターフェース
 */
interface NameFromInterface
{   
    /**
     * 名前を列挙型にマップします。
     *
     * @param string $name 名前
     * @return null|object 列挙型
     */    
    public static function tryNameFrom(string $name): ?self;
}
```

## 使用する列挙型に付与する

上がインターフェースを付けた場合、下が付けない場合。
どちらにせよ動きます。

ただ、**明確にそのメソッドがある**事を示したり、多態的な挙動をし易くなるので、インターフェースは付ける事をお勧めします。

```php
enum HogeBackedEnum: string implements NameFromInterface
{
    use NameFromTrait;

    case Fuga = 'ふが';
}

enum HogeEnum
{
    use NameFromTrait;

    case Fuga;
}
```

## 実際に動かす

全て一纏めにすると次の通り。
流石に長すぎるので、先に実行例を示します。

実行例：[PHP Playground](https://php-play.dev/?c=DwfgDgFmBQ0PQCoHQAQJYdCVCYxoaPVB2DILAqgskooC8AfJroJCag28aD0poC-GgtQyDHDIMMMg7QyCnDIPUMglwyBOhlQI40AGYBLAHYBDADYoAxvNkBnNSgCi0gK4BbAHKz9AUwBqC3aYCyssAGFZSiKegBvVCm%2BJk3-%2BgAAgBusgBOKOFhsgCeVPjEZJTYOHSAewzsgOsMXgFi-ihgYZKhAC6mKGolsiWSSpFh0XEAJADWpjGW8tZ2js6uZCgA2gC6ANyw%2Bb453uiA4MaAWdqA6gyA%2BgwpgDIMbFzcGZmA1gyAQgyA0QyA0gyAlgyAYgzHgEkMgCvxi4B%2BDICaDIBADNNob0Fg4SYVJUXSAHMUE1THp9PEPihAmFTCVdGFpCgAEYAexRigWK3Wmx4OwOJwuxw%2BeX8YF0SPktV%2B1Sp4l00iUNRRiIg6h0BgAFJV-kCQWCAJQALmRaPk008%2BXyMLhCJQAB0orEAPptGJK0wAD0klTUHL5BgANBVTPJxILBa12p1uvYnC5TPzxvkAL4TfxTSVzJarXBXRIUeLpLK7QDeboAmZIeLw%2BkMCX2i4JRSIAVqZGcDQQYIZ6odL4YjuTIgYBlBjwAkEgEWGYSekneMkUqmVGl1OkMpn52Fcv6F9MC4UFwHit7%2BcQomF9FB6jP6c1KdSmXX8yKafX6RcSyUbtQms0W1XW2y2vqmQYrmeqDTDE9TgC05DkZmG%2BVIPYMTo3KGd75Qbo3udlW9NM91DUN9vFdaYPXyL0sVwQBzBkxZZiDWI4zkuW5I1eT0YzjH5%2B15KcsygqEcPBPDgXvcpiBjP9EVRdEUAQlJ4O9JCUMJYlpjrSk6kbGpm3pRlJGZFBWV1MiVyNcSKKFEV0UHL9JHECcAEIFQaZVVXVLUdUnMFJKqPjzUtDorAPXp7X5Nch03bdzS3EpdIMR1rI-H9JRolBtXsrlbN3K1TJ6O1XCvMFLyaCjhmcl1pggpBIUYuCEKQwAIFUAF8DHkOQAghjwdCnkwqDsO%2BUiu0BF9wRSQr41%2BIoAURcKTEoohqNhPMUD0eR5AAH0TFM0wq6tOPJbjqT4lAW0E4SAQ7cSp30nlyIamSQB61MSnk99FJUtTGhVdotO1EpdQkkbaiMvcAsPCyrK-fIAJ3byVyi99wK-Dy7rO-yujMoLjxXMKIpQEAQDa3QOtA1zwI9dAUkARBVAEUGP4YmMMwADEwhRfRAE8nQByAxEMQ-lkSQShQZHTDRjGABVoiJjx-FivwiOIK4Ul2HYMOjbNYyK6ru3qswUCozmPParqVr63BiXyQb6x4gzaQEtsUER0nyf0Tt5r5h1hRAO71o3TaOWU9kjAa-dAqPc1RONnzAMFFRgMslB1xu7QwVJs3LtcOzpt8%2B2NCejcXvfDzjfdi7zK9wUpoc967fPNQjU1gOIdgKH4nh5WGtVwAShkAZ4ZAH6GQAfhkAVYZAHKGQuq1EaAZDKMJxGccoVfR-QAElpFr%2BulDcdw6cmOLsyZlm2byjmiK5qqpIagWmqFlrZRF7rk1Wwjcil-IuIbOX%2BNbITEUz1Hm-V3npO1u7xnAgiAAkUSmgAhZw2gAE2NvsSqBSR9DAeRTDMdvNCbjGbcO4Nw8NMXQW4SZZ2blTQmJRxjTFnBAlGugASyAGAAckAK4Max0Hn1gFfG%2BphjagPyOAxuUDKbUzgW5RB5RkGoLwdATWGD6GyFwbAUIYQlSPwMGADk0xr53wfqYZ%2BYJzT7zJofJORo4BwBQFODkgjTD3yUE-F%2BgpWH8gEYQ9REjVZ6mkkaDcsj5FgkUTosRGiUGyC0c5IAA&v=8.3&f=html)

```php
/**
 * 列挙型の名前 => 列挙型変換支援キャッシュクラス
 */
final class EnumNameValueMapCache
{
    /**
     * @var array 列挙型の名前 => 列挙型変換マップ
     */
    private static array $keyValueMapCache = [];

    /**
     * 指定した列挙型がキャッシュマップにあるかどうかを返します。
     *
     * @param string $enum 列挙型
     * @return bool 指定した列挙型がキャッシュマップにあるかどうか
     */
    public static function hasEnum(string $enum): bool
    {
        return \array_key_exists($enum, self::$keyValueMapCache);
    }

    /**
     * 指定した列挙型を名前 => 列挙型変換マップに登録します。
     *
     * @param object $enum 列挙型
     * @return string このクラスパス
     */
    public static function set(string $enum): string
    {
        foreach ($enum::cases() as $enum) {
            self::$keyValueMapCache[$enum::class][$enum->name]    = $enum;
        }        

        return self::class;
    }

    /**
     * 指定した列挙型で指定した名前があるかどうかを返します。
     *
     * @param string $enum 列挙型
     * @param string $name 名前
     * @return bool 指定した列挙型で指定した名前があるかどうか
     */
    public static function has(string $enum, string $name): bool
    {
        if (!\array_key_exists($enum, static::$keyValueMapCache)) {
            self::set($enum);
        }

        return isset(self::$keyValueMapCache[$enum][$name]);
    }
    
    /**
     * 指定した列挙型で指定した名前が合致するものを返します。
     *
     * @param string $enum 列挙型
     * @param strign $name 名前
     * @return null|object 列挙型
     */
    public static function get(string $enum, string $name): ?object
    {
        if (!\array_key_exists($enum, static::$keyValueMapCache)) {
            self::set($enum);
        }

        return self::$keyValueMapCache[$enum][$name] ?? null;
    }
}

/**
 * 列挙型向けtryNameFrom特性
 */
trait NameFromTrait
{   
    /**
     * 名前を列挙型にマップします。
     *
     * @param string $name 名前
     * @return null|object 列挙型
     */    
    public static function tryNameFrom(string $name): ?self
    {
        if (!EnumNameValueMapCache::hasEnum(self::class)) {
            EnumNameValueMapCache::set(self::class);
        }

        return EnumNameValueMapCache::get(self::class, $name);
    }
}

/**
 * 列挙型向けtryNameFromインターフェース
 */
interface NameFromInterface
{   
    /**
     * 名前を列挙型にマップします。
     *
     * @param string $name 名前
     * @return null|object 列挙型
     */    
    public static function tryNameFrom(string $name): ?self;
}

enum HogeBackedEnum: string implements NameFromInterface
{
    use NameFromTrait;

    case Fuga = 'ふが';
}

enum HogeEnum
{
    use NameFromTrait;

    case Fuga;
}

$name = 'Fuga';

var_dump(
    HogeBackedEnum::tryNameFrom($name), // enum(HogeBackedEnum::Fuga)
    HogeEnum::tryNameFrom($name),       // enum(HogeEnum::Fuga)
);
```

## putraits/enumsを用いた解決

`packagist`で配布している`putraits/enums`を利用すれば、わざわざファイルを用意するまでもありません。

https://packagist.org/packages/putraits/enums

自分の環境で`composer require putraits/enums`を実行し、後は次のサンプルのようにインターフェースとトレイトを設定するだけです。

```php
use putraits\enums\traits\NameFrom\NameFromInterface;
use putraits\enums\traits\NameFrom\NameFromTrait;

enum HogeBackedEnum: string implements NameFromInterface
{
    use NameFromTrait;

    case Fuga = 'ふが';
}

enum HogeEnum
{
    use NameFromTrait;

    case Fuga;
}

$name = 'Fuga';

var_dump(
    HogeBackedEnum::tryNameFrom($name), // enum(HogeBackedEnum::Fuga)
    HogeEnum::tryNameFrom($name),       // enum(HogeEnum::Fuga)
);
```