PHPのenum、便利なんだけどハマるところもある

PHP 8.1でenumが入ってから、もうだいぶ経ちました。ステータスや種別を表す定数の代わりにenumを使う、というのはもう当たり前の選択肢になったと思います。ただ実務で使ってみると、便利な反面「あ、これは知らないとハマるな」という挙動もいくつかあります。今日はそのあたりを整理してみます。

定数のenum化はやっぱり正解

以前はステータスをこう書いていました。

class OrderStatus
{
    const PENDING = 'pending';
    const PAID = 'paid';
    const SHIPPED = 'shipped';
}

これだと文字列を渡す側でタイプミスをしても気づけませんし、IDEの補完も弱いです。enumにするとこうなります。

enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
}

型で縛れるので、関数の引数に OrderStatus $status と書けば、存在しない値が紛れ込む心配がなくなります。この安心感は思った以上に大きいです。個人的には、新規実装でステータス系の定数を裸の文字列やintで持つ理由はもうほぼ無いと思っています。

DB保存時に「値」だけ見て「意味」を忘れがち

enumはDBには当然そのまま保存できないので、$status->value を保存し、取り出すときに OrderStatus::from() で戻す、という流れになります。ここでよくあるハマりどころが、DBに入っている値がenumの定義と食い違ったときです。

$status = OrderStatus::from($row['status']); // 該当するcaseがないとValueErrorで例外

過去のデータ移行漏れや、外部連携で想定外の値が入ってくるケースだと、ここで本番が落ちます。値が保証できない入力に対しては tryFrom() を使い、nullを許容した上でどう扱うか明示的に書いておくのが安全です。

$status = OrderStatus::tryFrom($row['status']) ?? OrderStatus::Pending;

「例外を握りつぶさない」というのが基本方針としては正しいはずですが、外部から来るデータを扱うレイヤーでは、tryFromで受けてからログを出す、という書き方のほうが実務では現実的だという気がしています。

enumはシリアライズすると素直じゃない

json_encodeにenumを渡すと、backed enumならvalueがそのまま出力されるので一見問題ないように見えます。ただし、enumを配列のキーには使えませんし、session保存やキャッシュへの保存で予期しない挙動に当たることがあります。

$counts = [];
foreach ($orders as $order) {
    $counts[$order->status->value][] = $order; // ->status では使えない
}

「enumをキーにできたら綺麗なのに」と思う場面は結構多いのですが、ここは素直にvalueに落として使う、という割り切りが必要です。無理にenumのままハッシュマップのキーにしようとして消耗した経験があるので、これは早めに諦めるのが吉です。

インターフェースを実装できるのは地味に強い

enumはメソッドを持てますし、interfaceも実装できます。これを使うと、ステータスごとの振る舞いの分岐をif文の羅列ではなくenum自身に持たせられます。

interface HasLabel
{
    public function label(): string;
}

enum OrderStatus: string implements HasLabel
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';

    public function label(): string
    {
        return match ($this) {
            self::Pending => '注文受付中',
            self::Paid => '支払い済み',
            self::Shipped => '発送済み',
        };
    }
}

表示ラベルや遷移可否の判定など、「値に紐づくロジック」をあちこちのswitch文にばらまかずenumに集約できるのは、地味ですがコードの見通しがかなり良くなります。match式に網羅性のチェックが効くので、新しいcaseを足し忘れたときにコンパイル時点(正確には実行時)で気づけるのもありがたいところです。

まとめ

enumは定数管理としての安心感が大きく、実務投入して後悔することはまずないというのが今のところの感触です。ただしDBや外部入力との境界では素直に例外を投げてくるので、tryFromで受けるかfromで受けるかは値の出どころをちゃんと意識して選ぶ必要があります。あとキーには使えない、というのだけ覚えておくと、変な設計に迷い込まずに済むと思います。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です