Laraveで独自バリデーションをつくる(例:ハンバーガーの注文)

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、Laravelは常に改良が加えられているので、少し複雑なことをするにしても工夫すればほぼ何でも対応ができてしまいます。

しかし、そうはいっても世の中の全てに対応できるわけではなく、中には自分でカスタマイズしないといけない場合もあります。

そして、今回はそんな「自分で実装しないといけない」お話として、

独自バリデーション

のつくり方をご紹介したいと思います。

・・・というのも、バリデーションのルールはメールアドレスやURLなどよく使うものは用意されているのですが、例えば「自社だけのルール」など複数条件をチェックしてやっと正しい入力かどうかがわかる、という場合もあるからです。

そこで❗

今回はLaravelで独自のバリデーション・ルールをつくる方法をご紹介したいと思います。

ぜひ皆さんのお役に立てましたら嬉しいです😊✨

「結婚式のお返しにもらった
お菓子、ウマウマでした👍」

開発環境: Laravel 8.x(細かなところに違いがありますが、基本的に以前のバージョンでも同じです)

やりたいこと

今回つくる独自バリデーションは、私もよく利用させてもらってる有名ハンバーガー・チェーンの注文を例にしたものです。

詳しいルールは次のとおりです。

  • モーニング商品は「5:00〜10:00」だけ注文OK
  • レギュラー商品は「5:00〜10:00」は注文できない(ただしフィッシュバーガーだけはいつでもOK)
  • ディーナー商品は、「17:00〜5:00」だけ注文OK
  • ドリンクはいつでも注文OK

※完全にマ●ドナルドをイメージしていますが商標権などの知識がないので、念のため似た感じで書きます😂

そして、使うデータは次のとおりです。

$foods = [
    ['id' => 1, 'name' => 'エッグマフィン', 'type' => 'morning'],
    ['id' => 2, 'name' => 'ソーセージマフィン', 'type' => 'morning'],
    ['id' => 3, 'name' => 'ハッシュポテト', 'type' => 'morning'],
    ['id' => 4, 'name' => 'てりやきバーガー', 'type' => 'regular'],
    ['id' => 5, 'name' => 'フィッシュバーガー', 'type' => 'regular'],
    ['id' => 6, 'name' => 'フライドポテト', 'type' => 'regular'],
    ['id' => 7, 'name' => '特大チーズバーガー', 'type' => 'dinner'],
    ['id' => 8, 'name' => '特大てりやきバーガー', 'type' => 'dinner'],
    ['id' => 9, 'name' => '特大フライドポテト', 'type' => 'dinner'],
    ['id' => 10, 'name' => 'コーラ', 'type' => 'drink'],
    ['id' => 11, 'name' => 'シェイク', 'type' => 'drink'],
    ['id' => 12, 'name' => '紅茶', 'type' => 'drink'],
];

では実際に開発していきましょう❗

独自バリデーション・ルールをつくる

では、今回利用する独自バリデーション・ルールFoodOrderをつくっていきます。

以下のコマンドを実行してください。

php artisan make:rule FoodOrder

すると、ファイルが作成されるので中身を以下のように変更します。

app/Rules/FoodOrder.php

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class FoodOrder implements Rule
{
    private $error_message = ''; // エラーメッセージを可変にするためのメンバ変数

    public function passes($attribute, $value)
    {
        $result = false;
        $foods = $this->getFoods(); // 本来はDBなどから取得してください
        $food_ids = $foods->pluck('id');
        $food_id = intval($value);

        if($food_ids->contains($food_id)) { // 選択した商品の存在チェックをしています

            $food = $foods->where('id', $food_id)->first();
            $food_type = $food['type']; // `morning`, `regular`, `dinner` or `drink`
            $hour = now()->hour;

            if($food_type === 'morning') { // モーニング商品の場合

                if($hour >= 5 && $hour < 10) { // 5:00 〜 10:00 なら true

                    $result = true;

                } else {

                    $this->error_message = 'モーニング商品が注文できるのは「5:00 〜 10:00」だけです。';

                }

            } else if($food_type === 'regular') { // レギュラー商品の場合

                if($food_id === 5) { // フィッシュバーガーはいつでも注文OK

                    $result = true;

                } else if($hour < 5 || $hour >= 10) { // 5:00 〜 10:00 「以外」なら true

                    $result = true;

                } else {

                    $this->error_message = 'レギュラー商品は「5:00 〜 10:00」の時間帯には注文できません。';

                }

            } else if($food_type === 'dinner') { // ディナー商品の場合

                if($hour < 5 || $hour >= 17) { // 17:00 〜 5:00 なら true

                    $result = true;

                } else {

                    $this->error_message = 'ディナー商品が注文できるのは「17:00 〜 5:00」だけです。';

                }

            } else if($food_type === 'drink') { // ドリンク商品の場合

                $result = true; // いつもで注文OK

            }

        } else {

            $this->error_message = 'この商品は存在していません。';

        }

        return $result; // ② true ならバリデーション通過、false ならエラー
    }

    public function message()
    {
        return $this->error_message; // ① 好きなエラーメッセージをセットできる
    }

    private function getFoods() {

        return collect([
            ['id' => 1, 'name' => 'エッグマフィン', 'type' => 'morning'],
            ['id' => 2, 'name' => 'ソーセージマフィン', 'type' => 'morning'],
            ['id' => 3, 'name' => 'ハッシュポテト', 'type' => 'morning'],
            ['id' => 4, 'name' => 'てりやきバーガー', 'type' => 'regular'],
            ['id' => 5, 'name' => 'フィッシュバーガー', 'type' => 'regular'],
            ['id' => 6, 'name' => 'フライドポテト', 'type' => 'regular'],
            ['id' => 7, 'name' => '特大チーズバーガー', 'type' => 'dinner'],
            ['id' => 8, 'name' => '特大てりやきバーガー', 'type' => 'dinner'],
            ['id' => 9, 'name' => '特大フライドポテト', 'type' => 'dinner'],
            ['id' => 10, 'name' => 'コーラ', 'type' => 'drink'],
            ['id' => 11, 'name' => 'シェイク', 'type' => 'drink'],
            ['id' => 12, 'name' => '紅茶', 'type' => 'drink'],
        ]);

    }
}

では、コードの内容をひとつずつご紹介していきましょう。

① message()

このメソッドから返された文字列がエラーメッセージになります。

そして、今回は条件によってエラーメッセージが変わるので、$error_messageという、どのメソッドからでもアクセスできるメンバ変数を用意し、その中身をreturnしています。

なお、なぜpublicではなくprivateにしているかというと、このエラーメッセージはクラスの外からはアクセスする必要がない(と判断した)からです。

このようにしてアクセスを遮断しておけば、もし間違ったコードを書いたてもエラーが表示されるので、開発の正確性が向上しますよ👍

② passes()

もしこのメソッドにtrueを返せば、バリデーションを通過したことになり、逆にfalseならバリデーションにひっかかるようになります。

なお、引数として取得できる$valueは送信データの値になりますので、もしfood_idという名前のデータにバリデーションをしたい場合、以下のような使い方になります。

$request->validate([
    'food_id' => [
        'required',
        new FoodOrder() // 👈 ここです
    ]
]);

__construct()を使う場合

今回__construct()は省略しましたが、例えばバリデーションするときにユーザー情報が必要な場合は、以下のようにして$userを送ることができます。

<?php

namespace App\Rules;

use App\Models\User;
use Illuminate\Contracts\Validation\Rule;

class FoodOrder implements Rule
{
    private $error_message = ''; // エラーメッセージを可変にするためのメンバ変数
    private $user;

    public function __construct(User $user)
    {
        $this->user = $user; // ここでメンバ変数へ格納
    }

メンバ変数に格納することで、いつでもどのメソッドからでも格納した情報にアクセスすることができるようになります。

// 省略

public function passes($attribute, $value)
{
    if($this->user->type === 'premium') { // 👈 ユーザー情報にアクセスできる

        return true; // プレミアム会員は時間に関係なく何でも注文できる

    }

// 省略

そして、使い方は次のとおりです。

$user = User::find(1);

$request->validate([
    'food_id' => [
        'required',
        new FoodOrder($user) // 👈 ユーザー情報も送る
    ]
]);

独自バリデーションを実装していく

では、ここまでで作った独自バリデーションを実装していきます。

ルートをつくる

routes/web.php

Route::get('custom_validation_create', [HomeController::class, 'custom_validation_create']);
Route::post('custom_validation_post', [HomeController::class, 'custom_validation_post']);

※ なお、Laravel 8.xではルートの指定方法が若干変更になっています。詳しくは以下のURLをご覧ください。

📝 Laravel 8.x の新機能・変更点のまとめ

コントローラーをつくる

次にコントローラーです。
以下のコマンドを実行してください。

php artisan make:controller HomeController

するとファイルが作成されるので中身を次のように変更します。

app/Http/Controllers/HomeController.php

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use App\Models\User;
use App\Rules\FoodOrder;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;

class HomeController extends Controller
{
    public function custom_validation_create() {

        return view('custom_validation_create');

    }

    public function custom_validation_post(Request $request) {

        // 👇 テスト用に「現在時間」を変更する
        $test_hour = 16;
        $dt = today()->setHour($test_hour);
        Carbon::setTestNow($dt);

        $request->validate([
            'food_id' => [
                'required',
                new FoodOrder()
            ]
        ]);

        return [
            'result' => 'バリデーション通過!'
        ];

    }
}

なお、バリデーションの方法は先ほど紹介したとおりですが、その他に重要なのが、「テスト用に現在時間を変更」している部分です。

これは、時間管理パッケージCarbonのテスト向け機能で、例えば今回の例では「今が16時としてシステムを実行」することができます。

ビューをつくる

最後にビューです。
以下のファイルを作成してください。

resources/views/custom_validation_create.blade.php

<html>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script>

    window.onload = () => {

        const food_id = 3;
        const url = '/custom_validation_post';
        const params = {
            food_id: food_id,
            _token: '{{ csrf_token() }}'
        };
        axios.post(url, params)
            .then(response => { // バリデーションに成功したとき

                console.log(response);

            })
            .catch(error => { // バリデーションに失敗したとき

                console.log(error);

            });

    };

</script>
</body>
</html>

この中でやっているのは、ページが表示されたらすぐにaxiosを使ってのAjax送信です。中身は目当てのfood_idと、LaravelCSRFトークン(不正アクセス防止用の文字列)になります。

ちなみに: データが空でもバリデーションを実行するには

ひとつ独自バリデーションで気をつけておくべきことは「送信データが存在しなければバリデーションは実行しない」、つまりバリデーションを通過してしまうという部分です。

そのため、以下のようにrequiredをつけていなければ、food_idが空、もしくは存在していない場合、バリデーションは通過してしまいます。

$request->validate([
    'food_id' => [
        new FoodOrder() // 👈 データがなければ通過してしまう😫
    ]
]);

また、今回のケースはfood_idというひとつだけのデータへのバリデーションだったので問題なかったですが、例えば複数のデータを総合的に見てバリデーションを判断する場合、「データがあろうがなかろうがバリデーションは必ず実行する」必要が出てくるかと思います。

そんな場合は以下のようにImplicitRuleを使ってください。

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\ImplicitRule; // 👈 追加しました

class FoodOrder implements ImplicitRule // 👈 変更しました
{

// 省略

こうすると、必ずバリデーションが実行されるようになるので、コードは以下のようにすることもできます。(実際にはfood_orderというデータは送信する必要はありません)

$request->validate([
    'food_order' => [
        new FoodOrder() // 👈 ここは必ず実行されます
    ]
]);

もちろんfood_orderの文字列はどんなものでもOKです。

そしてエラーが発生したとき、これと同じ名前になりますので可読性もよくなるかと思います。

テストしてみる

では、実際にテストしてみましょう❗

わざと間違えてみる

まずは、わざと間違えてバリデーションがきちんと拒否するかを確認します。

今回のコードでは、16時として動いているのでこの時間帯には注文できない「モーニング商品」のIDを送信してみましょう。

const food_id = 3; // 👈 ハッシュポテト

送信後、Google Chromeの開発ツールでチェックすると・・・・・・

はい❗16時にモーニング商品は注文できないので、正しくエラーを返すことができました。

注文できる注文で送信する

では、次に16時に注文できる「レギュラー商品」のIDを送信してみましょう。

const food_id = 4; // 👈 てりやきバーガー

送信すると・・・・・・

はい❗
今度は注文できる時間なので、予想通りバリデーションは通過しました。

成功です😊✨

ダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードできます。

Laraveで独自バリデーションをつくる

おわりに

ということで今回は、独自バリデーションの使い方をご紹介しました。

さすがに今回のサンプルのように複雑な独自ルールをつくることはめったにないとは思いますが、一度つくっておくと、他の入力で同じチェックが必要になっても、簡単に使い回しができて便利ですよ👍

そのため、独自バリデーションはある種「資産」といってもいいかもしれませんね。

ぜひ皆さんも活用してみてください。

ではでは〜❗

「明日アレ食べるぞ❗
…って思っても、起きたら欲しくないのは、
一体なんなんでしょうか(笑)」

この記事が役立ちましたらシェアお願いします😊✨ by 九保すこひ
また、わかりにくい部分がありましたらお問い合わせからお気軽にご連絡ください。
(また、個人レッスンも承ってます👍)
このエントリーをはてなブックマークに追加       follow us in feedly