再考:列挙型

はじめに

php で列挙型と言えば、 @hiraku さんの http://qiita.com/Hiraku/items/71e385b56dcaa37629fe みたいな実装がほとんどかと思います。

自分もだいたいこんな感じでいいかなと思っているのですが、2点ほどどうしても実現したいことがありました。

1. IDE で補完したい
2. 同じ定数値であっても異なる列挙型ならば別物としたい


これについて、第117回 PHP勉強会@東京 でLT したスライドがこちらです。

下記は、話した内容をもう少し詳しく書き起こしたものになります。

1. IDE で補完したい

そのままの話で、 __callStatic を利用した場合、補完できません。
補完できないということは、IDE のコード支援機能(例えばリファクタリング)が利用できないということです。

これについては、DocComment を入れれば済む話ではあります。

例えば、ゲームの難易度を表す列挙型があったとして、こんな感じにクラスコメントを追加することで対応可能です。

<?php
/**
 * @method static GameDifficulty HARD()
 * @method static GameDifficulty NORMAL()
 * @method static GameDifficulty EASY()
 */
final class GameDifficulty extends Enum
{
    const HARD = 'hard';
    const NORMAL = 'normal';
    const EASY = 'easy';
}

2. 同じ定数値であっても異なる列挙型ならば別物としたい

例えば、チーズの種別でハードを表すのに CheeseType::HARD としたとして、これはゲーム難易度のハード GameDifficulty::HARD とは別物です。
ところが、現状の列挙型実装だと

<?php
$hard = CheeseType::HARD();

var_dump($hard->valueOf() === GameDifficulty::HARD); // bool(true)

みたいなことが起きてしまいかねません。

では、下記のような比較用のメソッドを Enum クラスに生やしてはどうでしょう。

<?php
public function is(Enum $lhs)
{
    return get_class($this) === ($lhs)
        && $this->value() === $lhs->value();
}

これでも先の例よりはましです。
ただ、下記のようにミスした時に、(実行時ではなく)記述した時に気づけたほうが嬉しいですね。

<?php
$hard = CheeseType::HARD();

var_dump($hard->is(GameDifficulty::HARD())); // bool(false)

trait を利用した 列挙型

そんなわけで、以前、trait を使って getter や setter を IDE で補完できるようにしたことがあった *1 ので、その仕組みを利用して列挙型を実装してみました。

以下のように利用します。

同じようなことを2度(const と constant as)書かないとならないのが少々面倒ではありますが、クラスコメントで補完が効くようにする手間とそう変わらないのでいいかなと。

(後から気づいたけど、 コンストラクタを private にするなら、 コンストラクタでのチェックは不要かも)

EnumTrait の特徴

use … as でメソッドを再定義することで、IDE からそれぞれのメソッドが存在すると認識されます (少なくとも PhpStorm では認識されます)

trait を利用することで、`self` タイプヒンティング(型宣言)が、use されたクラスを表すことになるため、`is` メソッドが他のクラスを受け付けません。

また、シングルトンにすることで、オブジェクト自身の比較が定数値の比較と同じことになり、厳密な比較が可能になります

constant は、 `debug_backtrace` を使っているので、一見遅そうですが、 一度呼ばれた後はキャッシュ (static 変数) が効くため、呼び出し回数によっては `__callStatic` を呼ぶよりも早かったりします。

PhpStorm のバグ

ただ、残念ながら問題がありました。

PhpStorm では、 定数を生成するメソッドの利用箇所を参照する機能が利用できないのです。

バグレぽ出すも、なかなか修正されず。 


仕方ないので、 CommentDoc も併用しています。


また、

<?php
var_dump($difficulty->is(CheeseType::HARD())); // throw TypeError`

のコードは、静的にエラー出せるはずなのですが、
今の PhpStorm では trait 上の self は trait 自身 (つまり EnumTrait) と認識してしまうため、警告されません。

今後の進化に期待したいところ。

(あれ? ということは、結局現状の列挙型と大して変わらなくない?)

まとめ

従来の列挙型の欠点を trait を利用することで解消する試みを紹介しました。

残念ながら 今の PhpStorm による静的解析では、trait を適切に解釈しないためちょっとイマイチな部分もあります。