【Laravel + React】独自の Exception で擬似的なバリデーションをする

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

さてさて、私はLaravel 4.2からLoverなのですが、それから7年ぐらいたったいまでも「え!そんな機能あったの!?」というものがあります。

特にLaravelはマイナーアップデートでもガンガン新機能を追加してきたりするので、多すぎてすべてキャッチアップするのは難しいんですよね。(その分、調べたらほぼ何でもできちゃうのが魅力です👍)

そして、先日発見したのが、今回のテーマ

独自の Exception(例外)

です。

なんと、Laravelにはphp artisan make:exceptionという独自の例外を作成するコマンドがあるんですが、どうやら公式ドキュメントにもこのコマンド自体は載っていないようなんですね(2022.12.26現在)

そこで❗

今回は独自のExceptionをつくって、エラー処理を分離し、擬似的なバリデーションとして使ってみたいと思います。

ぜひ何かの参考になりましたら嬉しいです。😄✨

「サンタクロース(自分)から
良い子(自分)に Archiss の高級キーボード
を贈りました👍」

開発環境: Laravel 9.x、React、Vite、Inertia.js、TailwindCSS

やりたいこと

Laravelに限らず「あるデータを送信して登録」する場合、基本的には以下の手順で実装すると思います。

  1. データ送信
  2. バリデーションで中身チェック
  3. (OK なら)DB 登録

では、上の例にAPIへの登録も必要になるとどうなるでしょうか。

  1. データ送信
  2. バリデーションで中身チェック
  3. (バリデーションが OK なら)APIへデータ送信(登録)
  4. (API が OK なら)DB へ登録

この場合、もし3番のAPIの部分でエラーが返ってきたとすると4番目の「DB へ登録」には行かずエラー処理をすることになりますが、この部分を今回の独自例外で実装してみたいと思います。

実装するのは、今回のタイトルにもあるように「擬似的なバリデーション」です。

ぜひ楽しんでやっていきましょう❗

独自の例外(Exception)をつくる

いきなりですが、先に独自のExceptionをつくっていきます。

以下のコマンドを実行してください。(これは「APIにアクセスしたときにエラーになったよ」という意味の例外です)

php artisan make:exception ApiAccessErrorException

なお、作成したファイルの中身は変更しなくてOKです!

※ ちなみにLaravelの例外には通知機能などもありますので、ぜひその他の機能もチェックしてみてください。👉 本家のページ

コントローラーをつくる

続いて、コントローラーです。
ここで先ほどの独自例外ApiAccessErrorExceptionを使います。

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

php artisan make:controller CustomExceptionController

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

app/Http/Controllers/CustomExceptionController.php

<?php

namespace App\Http\Controllers;

use App\Exceptions\ApiAccessErrorException;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Inertia\Inertia;

class CustomExceptionController extends Controller
{
    public function create()
    {
        return Inertia::render('CustomException/Create')->with([
            'successMessage' => session('success'),
        ]);
    }

    public function store(Request $request)
    {
        // 通常のバリデーションは省略しています

        $api_url = route('custom_exception.access_error');
        $show_api_error = ($request->has_api_error === 'yes'); // API のエラーを出すかどうか

        try {

            $response = Http::get($api_url); // API にアクセス

            throw_if(
                $show_api_error,
                new ApiAccessErrorException($response->json('error_message'))
            );

            // 実際にはこの部分で DB 保存などを実行します

        } catch (ApiAccessErrorException $e) { // API でエラーが発生した場合

            $error_message = $e->getMessage();

            // 擬似的にバリデーションエラーを返す
            return back()->withErrors([
                'api_access' => $error_message
            ]);

        } catch (\Exception $e) { // その他のエラー

            throw $e;

        }

        return back()->with('success', '保存が完了しました!');
    }

    public function access_error()
    {
        $error_messages = [
            'API のパラメータに不足があります',
            'API のパラメータに誤りがあります',
            'API のパラメータに不正な値があります',
            'API キーが無効です',
            'API シークレットキーが無効です',
        ];
        $error_message = Arr::random($error_messages); // ランダムにエラーを返す

        return response([
            'error_message' => $error_message,
        ], 400);
    }
}

データ送信したときに実行されるstore()が今回のメインで、この中で「例外を使った分岐」をしていることに注目してください。

つまり、tryの中ではいくつかコードを実行しますが、APIにエラーがあったときだけcatch (ApiAccessErrorException $e) { ... }が実行されることになります。

※ 本来はAPIを使った処理はモデルやTraitなどに分割すると思うので、例外を投げるのは実際にその中ということになります。

また、DBへの保存などその他のエラーが出た場合は、次のcatch (\Exception $e){ ... }が実行されます。

※ 今回は複雑になるので書いていませんが、トランザクションで一気にDB保存する場合はこの中でロールバック(「ここまでのDB保存はナシよ」機能)することになります。

そして、APIエラーの場合、擬似的にバリデーションエラーを実行するわけですが、これはシンプルにwithErrors([ ... ])にエラーをセットしてリダイレクトすればOKです。(あとでReact側でこのエラーを受け取ることになります)

ビューをつくる

では、コントローラーでセットしたビューをつくっていきます。

resources/js/Pages/CustomException/Create.jsx

import {Inertia} from "@inertiajs/inertia";
import {useState} from "react";

export default function Create(props) {

    const errors = props.errors;
    const [hasApiError, setHasApiError] = useState('yes');
    const successMessage = props.successMessage;
    const handleSubmit = () => {

        const url = route('custom_exception.store');
        const params = {
            has_api_error: hasApiError,
        };
        Inertia.post(url, params)

    };

    return (
        <div className="p-5">
            {successMessage && (
                <div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-3" role="alert">
                    &#x1F44D; {successMessage}
                </div>
            )}
            {errors.api_access && (
                <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-3" role="alert">
                    &#x26A0; {errors.api_access}
                </div>
            )}
            <div className="mb-2 p-2">
                <div>
                    <label>
                        <input
                            type="radio"
                            value="has_api_error"
                            checked={hasApiError === 'yes'}
                            onChange={() => setHasApiError('yes')}
                        /> API でエラーを出す
                    </label>
                </div>
                <div>
                    <label>
                        <input
                            type="radio"
                            value="has_api_error"
                            checked={hasApiError === 'no'}
                            onChange={() => setHasApiError('no')}
                        /> なし
                    </label>
                </div>
            </div>
            <button
                type="button"
                className="text-white bg-blue-700 rounded-lg text-sm px-4 py-2"
                onClick={handleSubmit}>送信</button>
        </div>
    );

}

この中でやっているのは、シンプルに「ラジオボタンでデータを選択して送信」だけです。

そして、そのラジオボタンは以下2つの選択になっています。

  • API でエラーを出す
  • なし

つまり、テストしやすくしているだけですね👍

ルートをつくる

では、最後にルートをつくりましょう。

routes/web.php

// 省略

use App\Http\Controllers\CustomExceptionController;

Route::prefix('custom_exception')->controller(CustomExceptionController::class)->group(function(){

    Route::get('create', 'create')->name('custom_exception.create');
    Route::post('/', 'store')->name('custom_exception.store');
    Route::get('access_error', 'access_error')->name('custom_exception.access_error');

});

これで作業は完了です❗

お疲れ様でした😄👍✨

テストしてみる

では、実際にテストしてみましょう!
Viteを起動して、「http://******/custom_exception/create」へアクセスしてください。

すると、上のようになるので、「API でエラーを出す」のまま送信してみます。

すると・・・・・・

はい❗

うまくAPI用のエラーが返ってきました。(もちろん通常のバリデーションも同じロジックで表示することができます)

では、次に「なし」にして送信してみます。

うまくいくでしょうか・・・・・・??

はい❗
今度は保存完了の表示になりました。

成功です。✨😄👍

企業様へのご提案

今回の独自例外を使うと以下のようなメリットがあります。

  • エラー内容(今回の例で言うと、APIのアクセス失敗)などが特定できるのでバグ対応しやすい。
  • 特定の通知を実装することができるので、エラーが発生した詳細をリアルタイムで知らせることができる(メールやslackもOK)
  • 一度にたくさんの処理をする場合に例外の内容で分岐ができるのでメンテナンスしやすい。

こういった機能をご希望でしたらいつでもお気軽に「お問い合わせ」からご相談ください。

お待ちしてます😄✨

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

おわりに

ということで、今回は独自のExceptionを使ったサンプルを実装してみました。

システムが複雑になってくると一度にたくさんの処理をする必要が出てくると思いますが、今回のような「例外を使った分岐」は便利なので、ぜひ活用してみてくださいね。

ちなみに、まだまだLaravelにはドキュメントにも載っていないような情報が眠っていそうな気がします。(トレジャーハンター的な気分ですね😄)

ぜひこれからも優秀な方のツイッターなどで情報を集めていこうと思います。

ではでは〜❗

「残念ながら大阪梅田の
ストリートピアノ、
撤去されてました…涙」

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