ポンコツエンジニアのごじゃっぺ開発日記。

いろいろポンコツだけど、気にするな。エンジニアの日々の開発などの記録を残していきます。 自動で収入を得られるサービスやシステムを作ることが目標!!

【PHP8.1】LaravelのEnumバリデーションを使ってみる。

PHP8.1からEnum(列挙型)が使えるようになりました。

www.php.net

そのため、Laravel 8.xからEnumを使えるような機能がいくつかマージされてます。その中でも今回はリクエストパラメータなどバリデーションに対して、Enumを用いてチェックできるようになったので、それを試していきたいと思います。

日本語のドキュメントでいうとここになります。ただ、ここだけだと全然情報量が足りないので、ソースも見ながら理解していきたいと思います。

readouble.com

Unit Enum と Pure Enum と Backed Enum の違い

今回試していて、そもそもEnumの種類を理解してないとバリデーションの処理がうまく実装できなかったので、最初にUnit EnumPure EnumBacked Enumについて触れていきます。

www.php.net

Unit EnumとPure EnumとBacked Enumを並べて書いているが、主にはPure EnumとBacked Enumの違いが重要そうです。

Unit Enum

Enum全体にたいしての言葉っぽい?

Unit Enumインターフェイスは、全ての列挙型に対して、PHP のエンジンが自動的に適用するものです。このインターフェイスは、型チェックのためだけに存在しています。

www.php.net

interface UnitEnum {
    /* メソッド */
    public static cases(): array
}

例えば、こういうものになります。

<?php
enum Suit
{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;
}
?>

Pure Enum

値(value)を持たないnameというキーだけを持つenum。

そのため、インターフェイスはUnitEnumになる。

Backed Enum

namevalueというキーを持つenum。ドキュメントでは値に依存した列挙型という表現もされている。

www.php.net

デフォルトではEnumはスカラー値の情報は持たないが、スカラー値をもたせることも可能。そうすることで、データベースなどに読み書きするときに役に立ちますね。

例えば、以下のようなEnumです。

<?php
enum Suit: string
{
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';
}
?>

Backed Enumのインターフェイスは異なり、以下のようになる。

interface BackedEnum extends UnitEnum {
    /* メソッド */
    public static from(int|string $value): static
    public static tryFrom(int|string $value): ?static

    /* 継承したメソッド */
    public static UnitEnum::cases(): array
}

このように、スカラー型の値を列挙型にマップするfrom()やスカラー型の値を列挙型にマップを試みるtryFrom()が追加されています。

ここまでドキュメントを見るだけでも勉強になりますね。

LaravelのEnumのバリデーションを読み解く

次に、LaravelのEnumバリデーションの処理はどうなっているかを見ていきたいと思います。

Laravelのバージョンは8.78.1で見ていきます。

 php artisan --version
Laravel Framework 8.78.1

Illuminate\Validation\Rules\Enumpasses()が重要になると思うので、そこを見ていきたいと思います。

/**
 * Determine if the validation rule passes.
 *
 * @param  string  $attribute
 * @param  mixed  $value
 * @return bool
 */
public function passes($attribute, $value)
{
    if (is_null($value) || ! function_exists('enum_exists') || ! enum_exists($this->type) || ! method_exists($this->type, 'tryFrom')) {
        return false;
    }

    return ! is_null($this->type::tryFrom($value));
}

ここで、重要なことはif文のところです。

  • is_null($value)
    • valueがnullかどうか
  • ! function_exists('enum_exists')
    • enum_exists()メソッドが存在するか
  • ! enum_exists($this->type)
    • 指定したenumが存在するか
  • ! method_exists($this->type, 'tryFrom')
    • tryFrom()が存在するか

上記のどれかに一致しない場合はバリデーションはコケます。

ここで注目したいのは最後のtryFrom()の存在チェックです。tryFrom()があるかどうかというのは、上の方でのEnumの理解のときに紹介したBacked Enumインターフェイスかどうかにほぼ同等です(現時点では)。すなわち、valueを持たないPure Enumはバリデーションのチェックができない(必ず失敗する)ということです。

値を持たないEnumなのでサーバにどんな値を渡せば良いかがわからないですね。当たり前です。

ということで、Enumのバリデーションを使用したい場合はBacked Enumのみということになります(現時点で)。

実際にEnumのバリデーションを試す

では、実際にEnumとControllerを作成して試したいと思います。

作成したEnum

今回用意したEnumはPHPのドキュメントでも紹介されたトランプを用います。

Backed EnumとPure Enumを用意しました。

<?php

namespace App\Enums;

enum SuitBackedType: string
{
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';
}
<?php

namespace App\Enums;

enum SuitPureType
{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;
}

EnumなのでサフィックスにTypeをつけてみました。その直前にBackedかEnumがわかるように名前をつけています。

Pure Enumに対してのバリデーション

雑ですが、バリデーションの処理しかしないControllerです。

<?php

namespace App\Http\Controllers;

use App\Enums\SuitPureType;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules\Enum;

class EnumPureController extends Controller
{
    public function __invoke(Request $request)
    {
        $request->validate([
            'suit_type' => [new Enum(SuitPureType::class)],
        ]);

        return response([]);
    }
}

Pure Enumなので、これに対してどんな値を渡してもバリデーション失敗になり、エラーが返ってきてしまいます。

Backed Enumに対してのバリデーション

次に、Backed Enumのバリデーションの処理しかしないControllerです。

<?php

namespace App\Http\Controllers;

use App\Enums\SuitBackedType;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules\Enum;

class EnumBackedController extends Controller
{
    public function __invoke(Request $request)
    {
        $request->validate([
            'suit_type' => [new Enum(SuitBackedType::class)],
        ]);

        return response([]);
    }
}

これは、suit_typeにEnumで定義している'H', 'D', 'C', 'S'以外が渡されるとエラーになります。

動作確認で試したテストコードはこちらになります。

<?php

namespace Tests\Feature;

use Tests\TestCase;

class EnumBackedTest extends TestCase
{
    /**
     * @dataProvider data_enum
     * @param string|int $value
     * @param bool $expected
     */
    public function test_enum(string|int $value, bool $expected)
    {
        $response = $this->post('/enum_backed', [
            'suit_type' => $value,
        ]);

        self::assertEquals($expected, $response->isSuccessful());;
    }

    public function data_enum(): array
    {
        return [
            '正常系: HeartはEnumに含まれる' => [
                'value' => 'H',
                'expected' => true,
            ],
            '正常系: ClubsはEnumに含まれる' => [
                'value' => 'C',
                'expected' => true,
            ],
            '異常系: XはEnumに含まれない' => [
                'value' => 'X',
                'expected' => false,
            ],
            '異常系: 0はEnumに含まれない' => [
                'value' => 0,
                'expected' => false,
            ],
        ];
    }
}

このテストはすべてパスしているので、Enumに含まれるもののみ200で返ってきていることが確認できます。

最後に

Enumのバリデーションについて挙動を確認できました。また、個人的にEnumについてあまり理解できてなかったので、Enumについても少し勉強できたかなと思います。

まだEnum初心者なので、間違えているところがあるかもしれないので、間違いを見つけたらコメントで指摘していただけると嬉しいです。

お問い合わせプライバシーポリシー制作物