PHP8.3
までは動的かつ簡単にenum
からtryNameFrom
する事は出来ません。
と言う事でお手軽にtryNameFrom
しようじゃあないか。
なおオチとしてはcomposer require putraits/enums
する方が早いです。
詳細な解説はこちら。
注意
次の例の通りPHP8.3からは動的なenumアクセスが出来るのでこの記事は不要となります。
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
注意
本記事での実装では指定されたクラスパスがenumかどうかの判定を行っていません。
異常系に倒れた場合により多くの人が簡単に原因を特定できるようにするためです。
本来、定石としてはenum_exists
関数を用いて与えられたクラスパスの実在判定を行うべきです。
ですが、第二引数がtrue
、つまりオートロード可能でかつ、第一引数に攻撃コードを含む文字列を与えられた場合に、意図しないクラスファイルの読み込みを阻止する事ができません。
また、第二引数をfalse
にしてオートロードを抑止した場合、与えられたクラスパスが未読み込みのenumクラスパスだった場合、必ず存在しない
扱いになるためです。
キャッシャーの準備
列挙型ではプロパティを持つことが出来ません。
そのため、動的なキャッシュを持つことが出来ません。
ならば外に出してしまえばいい。
/**
* 列挙型の名前 => 列挙型変換支援キャッシュクラス
*/
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だけでも問題なく動きます。お好みや現場の状況に合わせてどうぞ。
/**
* 列挙型向け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;
}
実際に動かす
全て一纏めにすると次の通り。
流石に長すぎるので、先に実行例を示します。
実行例:PHP Playground
/**
* 列挙型の名前 => 列挙型変換支援キャッシュクラス
*/
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
を利用すれば、わざわざファイルを用意するまでもありません。
自分の環境でcomposer require putraits/enums
を実行し、後は次のサンプルのようにインターフェースとトレイトを設定するだけです。
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)
);