LaravelでのEnum型の使い方まとめ

# 記事を書いた動機

以下の経緯でEnumを社内に布教したかったから。

- Laravelを使う際、社内で定数をどこに書くか明確なルールが無かったので定義したかった
- configに書くか、Modelに書くか、定数用のクラスを作成してそこに書くか３つ案があったが、以下の理由によりEnumで管理するようにしたいと思った
    - `config()`は定義ジャンプができないので定数の値を見に行くのが面倒
    - 定数は条件分岐とかでいろんな場所から呼ばれるので、定数使うためだけにModelを呼ぶのはあまりやりたくない
        - 例えばBladeから`@foreach(App\Models\Order::STATUS_LIST as $key =>$val)` みたいに呼ぶのはいまいちかなと
    - 定数用のクラスで管理する場合、`app/Const/GlobalConst`みたいに普通のクラスで書いてもいいが、PHP8.1以上ならEnum型が使えるので今後は、Enumで管理する方が色々便利かなーと思った

# この記事で書くこと

- Laravelで定数をEnumで管理する際の具体的な使い方など

# この記事で書かないこと

- PHPのEnum型の詳しい仕様

# Enumってなに？

## 参考

https://www.php.net/manual/ja/language.enumerations.php

https://tech.designone.jp/entry/2022/01/12/181158

要点だけ抜粋すると

- EnumとはPHP8.1から利用できるようになった列挙型(Enumerations) で、取りうる値を限定した独自の型を定義できる
- クラスと同じく、メソッドを定義したり、インタフェースやトレイトを実装可能
- クラスと違って、 newを使って直接インスタンス化や、継承はできない
- スカラー値を持たないPure Enumとスカラー値を持つBacked Enum2種類ある
    - 現在config()に書いてるようなものは、基本的にBacked Enumだと思います
    - Backed Enumが持てるスカラー値にはいくつかの制約がある
        - intまたはstringの値のみを持つことができる（bool, floatは不可）
        - 単一の型のみを持つことができる（Union型は不可）
        - Backed Caseの値は全てユニークでなければならない
        - スカラー値はリテラルかリテラルを表す式でなければならない（定数はサポートしていない）

Pure Enum

```php
enum Suit
{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;
}

```

Backed Enum

```php
enum Suit: int
{
    case Hearts   = 1;
    case Diamonds = 2;
    case Clubs    = 3;
    case Spades   = 4;
}
```

- `Suit::Hearts->name`を呼び出すことで、`Hearts`の名前を取得できます
- `Suit::Hearts->value`を呼び出すことで、`Hearts`のスカラー値を取得できます

# LaravelでEnum使うとこんな感じ

## 前提

- 以降の説明はLaravel9以上の使用を前提としています

## 実装

1.まずApp/Enums/Role.phpを作成します。
- 日本語名表示用の`label()`メソッドを実装しています。
    - match式 （PHP8で入ったswitch文みたいなやつ）で書くとbreak要らないのでスッキリ書けます
        - switchと違って弱い比較(`==`)ではなく、 型と値の一致チェック(`===`) をします
        
https://www.php.net/manual/ja/control-structures.match.php
        

```php
namespace App\Enums;

enum Role: int
{
    case ADMIN = 1;
    case MEMBER = 2;

    public function label(): string
    {
        return match ($this) {
            Role::ADMIN  => '管理者',
            Role::MEMBER => '一般',
        };
    }
}
```

2.Enumで定義したいDBのカラムをModelでキャストする

App/Models/User.php

```php
use App\Enums\Role;

class User extends Model
{
    protected $casts = [
        'role' => Role::class,
    ];
```

## 基本的な使い方

```php
$user = User::find(1);

// キャストされたroleにアクセスするとEnum型で取得される
$user->role
// App\Enums\Role {#7622
//   +name:  "ADMIN"
//   +value: 1
// }

// name と value プロパティがある
$user->role->name // "ADMIN"
$user->role->value // 1

// 実装したlabel()メソッドの実行
$user->role->label() // "管理者"

// スカラー値からEnum~のcaseを返す
Role::tryFrom(1);  //Role::ADMIN
Role::from(1);  //Role::ADMIN

// スカラー値に対応するcaseが存在しない場合に、fromではErrorが生じますが、tryFromではnullを返します
Role::tryFrom(999);  //null
Role::from(999); //Error
```

## 定数として値だけ使うとき

```php
use App\Enums\Role;

// 管理者ユーザーだけ取得
$users = User::where('role', Role::ADMIN)->get();
```

```php
use App\Enums\Role;

if ($user->role === Role::ADMIN) {
	 // 管理者の場合の処理config
}
```

## **セレクトボックスやラジオボタンなどリスト表示したいとき**

- `cases()`メソッドを使うと以下のようにEnumの連想配列を全case取得できる
- これをループで回してvalueプロパティや日本語名表示用の`label()`を実行して表示する

```php
Role::cases();
// [
//   App\Enums\Role {#7620
//     +name: "ADMIN",
//     +value: 1,
//   },
//   App\Enums\Role {#7617
//     +name: "MEMBER",
//     +value: 2,
//   },
// ]
```

```php
<label for="role">権限</label>
@foreach (\App\Enums\Role::cases() as $role)
    <div class="custom-control custom-radio @error('role') is-invalid @enderror">
        <input type="radio" name="role" value="{{ $role->value }}"
            @checked((int) old('role') === $role->value)>
        <label>{{ $role->label() }}</label>
    </div>
@endforeach
```

\App\Enums\Role とBladeに書くのがだるいときは、以下のようにControllerから渡してあげても良いかと思います。

```php
    public function create(): View
    {
        $roleList = Role::cases();
        return view('admin.user.create', compact('roleList'));
    }
    
@foreach ($roleList as $role)
//
@endforeach

```

# config()には無いEnum使うと便利なところ

## カーソル合わせるだけで定義を確認できるし、定義ジャンプもできる

![スクリーンショット 2024-05-29 145821.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2655119/d69992c0-dcce-8a1a-000f-5e59526a4ab6.png)


## キャスト

https://readouble.com/laravel/11.x/ja/eloquent-mutators.html#enum-casting

- DBの値にEnumで定義した値以外が入ることを防げる
    - DBの制約では`tiny int`の場合、存在しないRoleである`3` が入ることを防げないが、Enumキャストを使うと実行時エラーになり防げます

```php
use App\Enums\Role;

class User extends Model
{
    protected $casts = [
        'role' => Role::class,
    ];
    
$user = find(1);
$user->role = 3; // 3 is not a valid backing value for enum App\Enums\Role.
```

## バリデーション

https://readouble.com/laravel/11.x/ja/validation.html#rule-enum

- `Enum`ルールは対象のフィールドに有効なenumの値が含まれているかどうかをバリデートします
    - `only`メソッドと`except`メソッドを使用し、有効な列挙ケースを制限できます
    
    ```php
    use App\Enums\ServerStatus;
    use Illuminate\Validation\Rule;
    
    $request->validate([
        'status' => [Rule::enum(ServerStatus::class)],
    ]);
    
    Rule::enum(ServerStatus::class)
        ->only([ServerStatus::Pending, ServerStatus::Active]);
    
    Rule::enum(ServerStatus::class)
        ->except([ServerStatus::Pending, ServerStatus::Active]);
    ```
    

## ルーティング

https://readouble.com/laravel/11.x/ja/routing.html#implicit-enum-binding

- ルート定義に[値に依存したBacked Enums](https://www.php.net/manual/ja/language.enumerations.backed.php)をタイプヒントすることができ、Laravelはそのルートセグメントが有効なEnum値に対応する場合のみルートを呼び出します。そうでない場合は、404 HTTPレスポンスが自動的に返されます
    - 下の例は、`{category}`ルートセグメントが、`fruits`または`people`である場合にのみ呼び出されるルートを定義している。それ以外の値は404になる。

```php
<?php

namespace App\Enums;

enum Category: string
{
    case Fruits = 'fruits';
    case People = 'people';
}
```

```php
use App\Enums\Category;
use Illuminate\Support\Facades\Route;

Route::get('/categories/{category}', function (Category $category) {
    return $category->value;
});
```

## Laravel11からartisanコマンドで生成できる

- 以下のmakeコマンドが追加されたのでenumもコマンドで作成できるようになります

```php
php artisan make:class
php artisan make:enum
php artisan make:interface
php artisan make:trait
```