【PHP】Enumの使い方|PHP 8.1の新機能とLaravelでの実践的な活用法
PHP 8.1 で導入された enum(列挙型)は、有限の選択肢を型として表現するための仕組みです。本記事では enum の基本から Laravel でのキャスト・バリデーションまでを整理します。コード例は PHP 8.1 以降、Laravel の例は Laravel 9 以降 を想定します(php -v と php 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+) |
|---|---|---|
| 型としての制約 | string や int のまま渡せる | 列挙ケース以外は IDE や PHPStan で検出しやすい |
| 取り得る値の一覧 | 定数を手動で列挙 | Status::cases() で取得できる |
| 振る舞いの追加 | 別クラスやヘルパー関数が必要 | enum 内にメソッドを定義できる |
| DB との対応 | 文字列のまま | Backed Enum でスカラー値と対応づけられる |
実行時には、Backed Enum の from() に未知の値を渡すと ValueError になるなど、境界で例外が起きることもあります。
enum は定義したケースだけが存在し、new では作れません。同じケースは常に同一インスタンスなので === で比較できます(公式マニュアル)。
enum の種類
Unit Enum(純粋な列挙)
スカラー値を持たない列挙です。名前(Pending、Shipped など)だけで識別します。リクエスト処理中の一時状態など、DB に保存しない用途向きです。
enum Status
{
case Pending;
case Shipped;
case Cancelled;
}
Backed Enum(値付き列挙)
string または int の 1 つのスカラー型 にバックする列挙です。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)— 一致するケースを返す。見つからなければValueErrortryFrom($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 なし、status は string):
$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 のバック型 は
stringかintのどちらか一方に限られる。混在はできない。 - シリアライズ では Backed Enum はスカラー値として扱われる。JSON API の返却形式はチームで揃える。
- レガシー移行 は、(1) 定数クラスと enum を並存させる → (2) 呼び出し側を enum に置換 → (3) 定数クラスを削除、の順が扱いやすい。DB 上の文字列値は Backed Enum の
valueと一致させる。
次に試すこと
- 既存の定数クラスを 1 つ選ぶ — 注文ステータスや権限など、取り得る値が有限なものから着手する。
app/Enums/に Backed Enum を定義する — DB や API の既存値に合わせてcaseとvalueを対応づける。- マイグレーションとモデルの
$castsを揃える — デフォルト値が enum のvalueと一致しているか確認する。 - Form Request で
Rule::enum()を入れる — 外部入力を enum 化する境界を決める。 - 表示用メソッドを enum に寄せる —
label()を足し、コントローラー内のmatch重複を減らす。
まとめ
- PHP 8.1 の enum は有限のケースを型として表現でき、Unit Enum と Backed Enum がある。
- Backed Enum は DB・API のスカラー値と対応づけやすく、
from()/tryFrom()で安全に復元できる。 - Laravel では Eloquent キャストと
Rule::enum()で、ドメインの状態をアプリ全体で一貫させやすい。 - 永続化には Backed Enum を使い、外部入力はバリデーションまたは
tryFrom()で未知の値を扱う。




