
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、私は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
に限らず「あるデータを送信して登録」する場合、基本的には以下の手順で実装すると思います。
- データ送信
- バリデーションで中身チェック
- (OK なら)DB 登録
では、上の例にAPI
への登録も必要になるとどうなるでしょうか。
- データ送信
- バリデーションで中身チェック
- (バリデーションが OK なら)APIへデータ送信(登録)
- (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">
👍 {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">
⚠ {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) - 一度にたくさんの処理をする場合に例外の内容で分岐ができるのでメンテナンスしやすい。
こういった機能をご希望でしたらいつでもお気軽に「お問い合わせ」からご相談ください。
お待ちしてます
おわりに
ということで、今回は独自のException
を使ったサンプルを実装してみました。
システムが複雑になってくると一度にたくさんの処理をする必要が出てくると思いますが、今回のような「例外を使った分岐」は便利なので、ぜひ活用してみてくださいね。
ちなみに、まだまだLaravel
にはドキュメントにも載っていないような情報が眠っていそうな気がします。(トレジャーハンター的な気分ですね)
ぜひこれからも優秀な方のツイッターなどで情報を集めていこうと思います。
ではでは〜
「残念ながら大阪梅田の
ストリートピアノ、
撤去されてました…涙」