Laravelのバリデーション・エラーメッセージをシンプル構成にする

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、Laravelがこれほど人気のフレームワークになったのはいろいろと理由があると思いますが、個人的にそのひとつとに「手軽にバリデーション(入力チェック)が使える」というのが入っています。

ただ、そこに一点だけ「こうできたらいいな」と思うことがあります。

それは・・・・・・

バリデーション・エラーの構成

です。

これは、最近メインで使うようになったAjax送信の話なのですが、バリデーション・エラーが発生すると次のようなレスポンスになります。

見ていただくとわかるとおり、各キーごとにエラーが「配列」で格納されていますね。

これは、複数のバリデーション・エラーが発生することを想定しているからです。

ただ、もちろんこれが正しいのですが、個人的には以下のようにもっとシンプル返って来てほしい時も結構あったりします。

↓↓↓ こんな感じです。

というのも、そもそもエラーが複数入ってることは少ないですし、複数エラーを全て表示してしまうと、テキストが多すぎて逆に内容を把握しにくいような気がするんですね。(好みの問題もあるんでしょうけど、@errorの場合もエラーはひとつですよね)

↓↓↓ こんな感じです。

そこで❗

今回はLaravelのバリデーションエラーの構成をシンプルにカスタマイズする方法をご紹介します。

ぜひ皆さんの参考になれば嬉しいです😊✨

「冬以外は、寝袋で寝ています👍」

開発環境: Laravel 8.x、Vue 3

Ajax送信するテスト環境をつくる

まず、今回データ送信してバリデーションを実行できるテスト環境をつくっていきます。(不要な方は次の項目まで飛ばしてください)

なお、今回はテストとしてitemsテーブルにデータ保存する場合を想定しています。

コントローラーをつくる

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

php artisan make:controller ItemController

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

app/Http/Controllers/ItemController.php

<?php

namespace App\Http\Controllers;

use App\Http\Requests\ItemRequest;
use Illuminate\Http\Request;

class ItemController extends Controller
{
    public function create() {

        return view('item.create');

    }

    public function store(ItemRequest $request) {

        // ここでデータを保存

    }
}

ビューをつくる

続いてビューです。
作成のコマンドはないので、直接ファイルを作成してください。

resources/views/item/create.blade.php

<html>
<head>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
</head>
<body>
<div id="app" class="p-5">
    <div class="form-group">
        <label>名前</label>
        <input type="text" class="form-control" v-model="params.name">
    </div>
    <div class="form-group">
        <label>説明文</label>
        <textarea type="text" class="form-control" v-model="params.description"></textarea>
    </div>
    <div class="form-group">
        <label>価格</label>
        <input type="number" min="0" class="form-control" v-model="params.price">
    </div>
    <div class="form-group">
        <button type="button" class="btn btn-primary" @click="onSubmit">送信</button>
    </div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script>
<script src="https://unpkg.com/vue@next"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script>

    Vue.createApp({
        el: '#app',
        data() {
            return {
                params: {
                    name: '',
                    description: '',
                    price: 0
                }
            }
        },
        methods: {
            onSubmit() {

                const url = '/item';
                axios.post(url, this.params)
                    .then(response => {

                        // 成功した時

                    })
                    .catch(error => {

                        // 失敗した時

                    });

            }
        }
    }).mount('#app');

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

ルートをつくる

最後にルートです。

routes/web.php

<?php

// 省略

// 👇 以下を追加
Route::get('item/create', [\App\Http\Controllers\ItemController::class, 'create']);
Route::post('item', [\App\Http\Controllers\ItemController::class, 'store']);

これでテスト環境ができあがりました😊

独自のリクエストをつくる

では、ここからが「シンプルなバリデーション・エラー」をつくる部分になります。

まず、以下のコマンドで独自のFormRequestをつくります。

php artisan make:request ItemRequest

するとリクエスト・ファイルが作成されるので中身を以下のように変更してください。

app/Http/Requests/ItemRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class ItemRequest extends FormRequest
{
    public function authorize()
    {
        return true; // 👈 今回、認証チェックはしないので true へ変更。
    }

    public function rules()
    {
        // 👇 ここがチェックするバリデーション・ルールです。お好みで変更してください。
        return [
            'name' => ['required'],
            'description' => ['required'],
            'price' => ['required', 'integer', 'min:1']
        ];
    }

    // Override
    protected function failedValidation(Validator $validator)
    {
        $errors = collect($validator->errors());
        $messages = $errors->map(function($error_messages){

            return $error_messages[0];

        });

        throw new HttpResponseException(response(
            $messages,
            422
        ));
    }
}

ここで重要なのが、failedValidation()です。

このメソッドはバリデーションが失敗した時に呼ばれることになっていて、ここを上書きすることで「シンプルなエラーメッセージ」を実装しています。

やっている手順は以下のとおりです。

  1. Validatorから全エラーメッセージを取得
  2. それらの中から、それぞれ最初のものだけを取得
  3. 取得したメッセージをレスポンスで返す

たったこれだけでOKです。

より汎用的に使えるようにする

先ほどは、failedValidation()を独自リクエストの中に直接登録しました。

もちろん数が少ない場合は、これで問題ないのですが、今後作成するバリデーションにも同じく「シンプルなエラーメッセージ」を実装する場合、毎回同じコードを書かなくてはいけなくなります。

これでは、保守管理も難しくなりますし、何より「めんどくさい😫」ですよね。

そこで、せっかくですので汎用的に使えるようにしてみましょう。

使うのはTraitです。

Traitというのは、簡単にいうと複数のクラスで特定のコードを使い回しするためのものです。

では実際にやってみましょう!
まずapp/Traitsフォルダをつくり、以下のファイルを追加します。

app/Traits/SingleValidationErrorMessage.php

<?php

namespace App\Traits;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

trait SingleValidationErrorMessage {

    protected function failedValidation(Validator $validator)
    {
        $errors = collect($validator->errors());
        $messages = $errors->map(function($error_messages){

            return $error_messages[0];

        });

        throw new HttpResponseException(response(
            $messages,
            422
        ));
    }

}

なお、もちろんfailedValidation()の中身は先ほどと全く同じものになります。

では、これを先ほどのItem.phpで使えるようにしてみましょう。

<?php

namespace App\Http\Requests;

use App\Traits\SingleValidationErrorMessage; // 👈 ここを追加しました。
use Illuminate\Foundation\Http\FormRequest;

class ItemRequest extends FormRequest
{
    use SingleValidationErrorMessage; // 👈 ここを追加しました。

    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'name' => ['required'],
            'description' => ['required'],
            'price' => ['required', 'integer', 'min:1']
        ];
    }

    // 👉 failedValidation()は削除しました
}

つまり、SingleValidationErrorMessage()を呼び出すだけで、failedValidation()を追加する必要はなくなるということになります。

これで、簡単に使えますし、もしバグがあってもTraitの中身を変更すれば全ての場所に反映されるので省力化ができますね👍

テストしてみる

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

まず「http://******/item/create」にアクセスします。

そして、今回はバリデーションのテストですので、そのままの状態で送信してみます。

では、送信結果を開発ツールで見てみましょう。

はい!
うまくキーごとにひとつだけのエラーを取得することができました。

成功です😊✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はいつもとは少し違った内容の記事をお届けしました。

ちなみに、通常返される配列の場合はJavaScript内で以下のようにして格納し直しています。

let errors = {};

for(let key in error.response.data.errors) {

    let responseError = error.response.data.errors[key];
    errors[key] = responseError[0];

}

this.errors = errors;

もしVueなどで使う場合は、今回のTraitのようにmixinを作っておいて、そこから使い回しできるようにしておくのもいいかもしれません。

ぜひいろいろと工夫してより楽に開発できるようにしていってくださいね。

ではでは〜❗

「コーヒーを濃くしすぎた結果、
胃がキリキリ言ってます・・・😭」

このエントリーをはてなブックマークに追加       follow us in feedly