【PHP】Enumの使い方|PHP 8.1の新機能とLaravelでの実践的な活用法

PHP 8.1 で導入された enum(列挙型)は、有限の選択肢を型として表現するための仕組みです。本記事では enum の基本から Laravel でのキャスト・バリデーションまでを整理します。コード例は PHP 8.1 以降、Laravel の例は Laravel 9 以降 を想定します(php -vphp artisan --version で環境を確認してください)。詳細は PHP 公式マニュアルの列挙型の節 および Laravel の Eloquent キャストのドキュメント を参照してください。

はじめに

注文ステータスを 'pending''shipped''cancelled' の文字列で扱い、定数クラスやマジックストリングを散らばらせていませんか。タイポや未定義値の混入は、テストで拾えても本番ログの調査コストになります。

PHP 8.1 以前は class OrderStatus { public const PENDING = 'pending'; ... } のような定数クラスが定番でした。型としての列挙 は言語組み込みではありませんでした。PHP 8.1 から enum が使えるようになり、OrderStatus::Pen と入力すれば IDE が Pending を補完するなど、有限の状態をコード上で明示しやすくなりました。

この記事で扱う範囲は次のとおりです。

  • Unit Enum と Backed Enum の違いと基本構文
  • cases()from()tryFrom()match 式との組み合わせ
  • enum へのメソッド追加
  • Laravel でのキャスト、バリデーション、マイグレーションの例
  • よくある失敗例と次に試すこと

背景:定数クラスから enum へ

定数クラスは手軽ですが、次のような弱点がありました。

観点定数クラスenum(PHP 8.1+)
型としての制約stringint のまま渡せる列挙ケース以外は IDE や PHPStan で検出しやすい
取り得る値の一覧定数を手動で列挙Status::cases() で取得できる
振る舞いの追加別クラスやヘルパー関数が必要enum 内にメソッドを定義できる
DB との対応文字列のままBacked Enum でスカラー値と対応づけられる

実行時には、Backed Enum の from() に未知の値を渡すと ValueError になるなど、境界で例外が起きることもあります。

enum は定義したケースだけが存在し、new では作れません。同じケースは常に同一インスタンスなので === で比較できます(公式マニュアル)。

enum の種類

Unit Enum(純粋な列挙)

スカラー値を持たない列挙です。名前(PendingShipped など)だけで識別します。リクエスト処理中の一時状態など、DB に保存しない用途向きです。

enum Status
{
    case Pending;
    case Shipped;
    case Cancelled;
}

Backed Enum(値付き列挙)

string または int1 つのスカラー型 にバックする列挙です。DB カラムや API の値と対応づけるときに使います。永続化には Backed Enum を使うのが一般的です。

// app/Enums/OrderStatus.php(本記事では app/Enums/ に置く慣習に従う)
enum OrderStatus: string
{
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';
}

Backed Enum では ->value でスカラー値、->name でケース名(文字列)を取得できます。Unit Enum には value プロパティはありません。

基本的な使い方

ケースの参照と比較

$status = OrderStatus::Pending;

if ($status === OrderStatus::Pending) {
    // 処理
}

すべてのケースを列挙する

cases() は定義順にケースの配列を返します。

foreach (OrderStatus::cases() as $case) {
    echo $case->name;   // Pending, Shipped, ...
    echo $case->value;  // pending, shipped, ...(Backed Enum のみ)
}

Backed Enum の生成:from()tryFrom()

文字列や整数から enum に変換するときは、Backed Enum の静的メソッドを使います(公式マニュアル)。

  • from($value) — 一致するケースを返す。見つからなければ ValueError
  • tryFrom($value) — 一致するケースを返す。見つからなければ null
$status = OrderStatus::from('pending');      // OrderStatus::Pending
$unknown = OrderStatus::tryFrom('invalid');  // null

match 式との組み合わせ

match は PHP 8.0 以降の式で、分岐結果を直接返せます。enum のケースを左辺に書き、すべてのケースを列挙すると網羅性を保ちやすいです。コントローラー内の一時的な分岐に使う例です。

$label = match ($status) {
    OrderStatus::Pending => '処理待ち',
    OrderStatus::Shipped => '発送済み',
    OrderStatus::Cancelled => 'キャンセル',
};

enum にメソッドを追加する

enum にも通常のクラスと同様に メソッド を定義できます。インターフェースの実装や trait の利用も可能です(継承はできません)。先ほどの match 分岐を label() メソッドに閉じ込めると、表示ラベルをドメイン側に集約できます。

enum OrderStatus: string
{
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';

    public function label(): string
    {
        return match ($this) {
            self::Pending => '処理待ち',
            self::Shipped => '発送済み',
            self::Cancelled => 'キャンセル',
        };
    }

    public function isFinal(): bool
    {
        return $this === self::Shipped || $this === self::Cancelled;
    }
}

echo OrderStatus::Pending->label(); // 処理待ち

Laravel での実践

Laravel プロジェクトでは、enum を app/Enums/ に置くパターンが多いです。Laravel 11 以降では php artisan make:enum OrderStatus で雛形を生成できます。以下、注文ステータスを enum 化する最小の流れを示します。

1. マイグレーション

Backed Enum の value に合わせてカラム型を選びます。文字列なら string、整数なら unsignedTinyInteger などが一般的です。

Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->string('status')->default('pending');
    $table->timestamps();
});

本記事の主路線は string カラム + アプリ層で enum 検証 です。PostgreSQL などでは CHECK 制約で値を縛る選択肢もあります。MySQL の ENUM 型と PHP の enum は別物なので、混同しないでください。

2. モデルにキャストを追加

モデルの $casts に enum クラスを指定すると、取得時は enum、保存時はスカラー値(Backed Enum の value)へ変換されます(Laravel ドキュメント)。

<?php

namespace App\Models;

use App\Enums\OrderStatus;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    protected $casts = [
        'status' => OrderStatus::class,
    ];
}
$order = Order::find(1);
$order->status; // OrderStatus インスタンス

$order->status = OrderStatus::Shipped;
$order->save(); // DB には 'shipped' が保存される

3. バリデーションと取得

Laravel 9 以降では Illuminate\Validation\Rules\Enum で、リクエスト値が有効なケースか検証できます。$request->enum()バリデーション通過後 に enum として取得するメソッドです。単体では検証しません。

<?php

namespace App\Http\Requests;

use App\Enums\OrderStatus;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UpdateOrderStatusRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'status' => ['required', Rule::enum(OrderStatus::class)],
        ];
    }
}
// Controller
public function update(UpdateOrderStatusRequest $request, Order $order)
{
    $status = $request->enum('status', OrderStatus::class);
    $order->status = $status;
    $order->save();

    return response()->json(['label' => $status->label()]);
}

コントローラー内で直接検証する場合は、次の順序になります。

$request->validate([
    'status' => ['required', Rule::enum(OrderStatus::class)],
]);
$status = $request->enum('status', OrderStatus::class);

4. ファクトリやテスト

// database/factories/OrderFactory.php
public function definition(): array
{
    return [
        'status' => OrderStatus::Pending,
    ];
}
Order::factory()->create();
$this->assertEquals(OrderStatus::Pending, $order->fresh()->status);

JSON API での返し方

Backed Enum をそのまま JSON にすると、多くの場合スカラー value が返ります。

json_encode(OrderStatus::Shipped); // "shipped"

フロント向けにラベルも返す場合は、Resource などで形を揃えます。

return [
    'value' => $order->status->value,
    'label' => $order->status->label(),
];
// 例: ["value" => "shipped", "label" => "発送済み"]

よくある失敗例

$casts 未設定のまま文字列を代入した

Before($casts なし、statusstring):

$order->status = 'pendng'; // typo
$order->save(); // DB に誤った文字列がそのまま入る

After(OrderStatus::class にキャスト済み):

$order->status = OrderStatus::Pending; // OK
$order->status = 'pendng';             // TypeError などで代入時に失敗しやすい

外部入力は Rule::enum() または tryFrom() で検証し、モデルには常にケース経由で代入するのが安全です。

Unit Enum を DB に保存しようとした

Eloquent の enum キャストは Backed Enum 前提 です。Unit Enum を string カラムにキャストして保存しようとすると、value がないため ValueError などの例外になります。

enum Phase { case Draft; case Published; }

// $casts => ['phase' => Phase::class] + string カラム → 保存時に例外

永続化が必要なら Backed Enum を使い、名前だけで足りる内部状態は DB に保存せずアプリ内だけで扱います。

from()tryFrom() の使い分けミス

ユーザー入力や API の未知の値に from() を使うと ValueError になります。境界では tryFrom() で null を扱うか、バリデーションで弾いてから from() を使うとよいです。

実装時の注意点

  • PHP 8.1 未満の環境 では enum は使えない。実行環境と CI のバージョンを先に確認する。
  • Backed Enum のバック型stringint のどちらか一方に限られる。混在はできない。
  • シリアライズ では Backed Enum はスカラー値として扱われる。JSON API の返却形式はチームで揃える。
  • レガシー移行 は、(1) 定数クラスと enum を並存させる → (2) 呼び出し側を enum に置換 → (3) 定数クラスを削除、の順が扱いやすい。DB 上の文字列値は Backed Enum の value と一致させる。

次に試すこと

  1. 既存の定数クラスを 1 つ選ぶ — 注文ステータスや権限など、取り得る値が有限なものから着手する。
  2. app/Enums/ に Backed Enum を定義する — DB や API の既存値に合わせて casevalue を対応づける。
  3. マイグレーションとモデルの $casts を揃える — デフォルト値が enum の value と一致しているか確認する。
  4. Form Request で Rule::enum() を入れる — 外部入力を enum 化する境界を決める。
  5. 表示用メソッドを enum に寄せるlabel() を足し、コントローラー内の match 重複を減らす。

まとめ

  • PHP 8.1 の enum は有限のケースを型として表現でき、Unit Enum と Backed Enum がある。
  • Backed Enum は DB・API のスカラー値と対応づけやすく、from() / tryFrom() で安全に復元できる。
  • Laravel では Eloquent キャストと Rule::enum() で、ドメインの状態をアプリ全体で一貫させやすい。
  • 永続化には Backed Enum を使い、外部入力はバリデーションまたは tryFrom() で未知の値を扱う。

Source