背景
当初の実装
-
以下の一連の処理を実行するプログラム
- 1, 「製品」リストから「注文」ボタン押下
- 2, 同じルートにPOST送信を実行
- 3, 2,のリクエストの中にorder_idを一緒に送る
- 4, そのidをもとに「製品」テーブルからデータを取得
- 5, 4,で取得した「製品」データを「カート」テーブルに格納
- 6, 5,の保存処理が終了すると、「カート」一覧に移動
-
この状況で3, での想定外処理であるリクエスト値の書き換えに対して、4, の実装時に例外処理を実装していた
-
例外処理にしていた理由は以下の考えによる
- 基本的にユーザーはここで入力処理をすることはないので、エラーが起きる可能性は非常に低い
- ここでエラーが発生するのは基本的に「ユーザーに悪意がある場合」のみ
- この「ユーザーに悪意がある場合」というのはもはや開発者の想定外の事象
開発環境
※注意
状況をイメージできるために書いているので、正常に動くかは検証していないです。ご了承ください。
環境
- PHP 8.0.4
- Laravel 8.x
DB
必要最低限だけ書いてます。
- Userテーブル
カラム名 | 内容 | 補足 |
---|---|---|
id | ユーザーID | PK |
... | ... | ... |
- 製品(Product)テーブル
カラム名 | 内容 | 補足 |
---|---|---|
id | 製品ID | PK |
name | 製品名 | |
path | 画像パス | |
... | ... | ... |
- カート(Cart)テーブル
カラム名 | 内容 | 補足 |
---|---|---|
id | カートID | PK |
product_id | 製品ID | FK |
... | ... | ... |
- ※前提
- userとcartはリレーション構築済み
ソースコード
- ルート
// 製品一覧表示
Route::get('/products', [App\Http\Controllers\ProductController::class, 'index'])->name('products.index');
// 製品をカートに保存
Route::post('/products', [App\Http\Controllers\ProductController::class, 'store'])->name('product.store');
- コントローラ
- カート関連のコントローラはビューファイルを
cart/index.blade.php
を表示するだけなので割愛
- カート関連のコントローラはビューファイルを
class ProductController extends Controller
{
/**
* 製品一覧表示
*/
public function index()
{
// 製品一覧情報
$products = Product::get();
return view('products.index', compact('products'));
}
/**
* 製品をカートに保存
*/
// フォームリクエストバリデーション実装
public function store(Request $request)
{
$validated = $request->validated();
// ユーザー情報取得
$user = User::findOrFail($validated['user_id']);
// ★例外処理開始
try {
// ★製品idに紐づく製品情報を取得
// Note: ココで製品IDが異常時もキャッチできるようにする
$product = Product::findOrFail($validated['product_id']);
// 製品をカートに保存
// Note: userとcartはリレーション構築済み
$user->cart->fill([
'product' => $product,
]);
$user->cart->save();
DB::commit();
} catch (Throwable $e) {
DB::rollback();
throw $e;
}
// カート一覧に遷移
return redirect()->route('cart.index');
}
- ビュー
- カート関連のビューファイルは本題と関係ないので割愛
↓カート一覧
<body>
<div>
@foreach ($products as $product)
<form action="" method="POST">
// 画像表示
<img src= "/img/{{ $product->path }}">
// 製品名表示
<div>{{ $product->name }}</div>
<div>
// hiddenでリクエスト時に製品IDを一緒に送る
<input type="hidden" name="productId" value="{{ $product->id }}">
<input type="submit" value="送信">
</div>
</form>
@endforeach
</div>
</body>
指摘
- これに対して、レビュワーから
「リクエスト値にはバリデーションを実装しようか」
という指摘をもらった
- レビュワーの考えは以下の通り
- 「『ユーザーに悪意がある場合』というのはもはや開発者の想定外の事象」という認識は合っている
- ただ、これだと『ユーザーに悪意がある場合』と『システム要因によるエラー(DBの障害など)』が一緒くたになるのは問題
- この時Throwableを投げてるのみで例外処理をしていた
- さらに、そもそも、リクエストの異常はバリデーションで防げるからこっちのが理想的
- フォームリクエストバリデーションを使用すれば、次のビジネスロジック(Controller)の処理に入る前にレスポンスを返せる
- 特に
『ユーザーに悪意がある場合』と『システム要因によるエラー(DBの障害など)』が一緒くたになるのは問題
と
「リクエストの異常はそもそもバリデーションで防げる」
の2点については納得して修正。
- だが、同時に
「じゃあ、そもそも『例外』と『バリデーション』が想定する使われ方ってなんだろ?」
と素朴な疑問が浮かんだので、メモ
今回解決したいギモン
例外
- 「当初実装していた例外処理の範囲ってどこまで?例外ってどういう状況?」
バリデーション
- 「『入力』以外のアクション(今回のような『ボタン押下』)に対してバリデーションを使うべきか」
内容
例外
概要
プログラムも失敗します。
…
大惨事になる前に気付くことができるようにプログラミング言語にもガス漏れ検知器のような「失敗を伝えるしくみ」が必要なのです。
- IT用語辞典 e-Words
プログラムが通常の処理では想定していない事態や事象を「例外」(exception)と呼び、例外が生じた時の対応を記述したコードを例外処理という
https://e-words.jp/w/例外処理.html
個人的な解釈
-
e-Words
で述べられる「『想定外の処理』が実行されたときの処理」が正しそう - 以下認識であながち間違いなさそう
- 想定できる処理:バリデーション
- 上記以外(=想定できない処理):例外
- 本題について、コーディングを支える技術 ~成り立ちから学ぶプログラミング作法には以下2つの状況を「例外を投げる状況」としている
- 関数呼び出し時に不足している場合
- 配列の範囲外を取得しようとした場合
- ただ同書は言語ごとに例外の扱いが違う(返り値が違う、など)ことから
「何が『例外的状況か』は正解がない」
と結論付けている
結論
「当初実装していた例外処理の範囲ってどこまで?例外ってどういう状況?」
👇
💡 ぶっちゃけ「コレ!」という正解はない。 しいて言うならバリデーション以外でリクエストが飛んでくる状況
バリデーション
概要
いくつか情報ソースがあったので、列挙
- ISO 9000
「観察・測定・試験などの手段によって得られた客観的証拠を提示して、利害関係者が意図する用途・適用に関する要求事項が満されていることを実環境あるは模擬環境で確認すること」
※参考 https://www.itmedia.co.jp/im/articles/1111/07/news145.html
- IEEE Std 610.12
「開発プロセスの途中または最後に、利用者のニーズや意図された利用法などの要求事項を満たしているかを決定するために、システム/コンポーネントを評価するプロセス」
※参考 https://www.itmedia.co.jp/im/articles/1111/07/news145.html
- IT用語辞典 e-Words
記述・入力されたデータが、あらかじめ規定された条件や仕様、形式などに適合しているかどうかを検証・確認することを表す。
※参考 https://e-words.jp/w/バリデーション.html
個人的な解釈
-
ISO 9000
やIEEE
はプロダクト全体のことを指して述べられているが、具体化して同じことが言えると思う - 特に、
IEEE
の「利用者のニーズや意図された利用法などの要求事項を満たしているか」という点を抜粋すれば、バリデーションの対応範囲はe-Words
が述べている「記述・入力されたデータ」のみではなさそう - 以上の文献からバリデーションとは
データの受け手が受け取る対象が、想定する形式のモノか否かを正否判断する
という一つの解釈を導けて、送られる形式は問わない、と言えそう。
結論
「『入力』以外のアクション(今回のような『ボタン押下』)に対してバリデーションを使うべきか」
👇
💡 ボタン押下でリクエスト値が送られてくるので、これも一つの入力値と捉え、バリデーションの管轄内である
- これにより、冒頭のレビュワーの指摘である
「リクエストの異常はそもそもバリデーションで防ぐ」
というスタンスはより説得力を帯びました。
- なので、「全てのエラー系を例外処理で処理」から「バリデーションも使用」という方向性に修正し
// 追加 : フォームリクエストバリデーション実装
public function store(ProductStoreRequest $request) {
...
...
...
}
とし、製品IDの異常値をコントローラ処理に入る前にはじくようにしました。
所見
- 具体的事象を乗せたとしても、かなり抽象的なアプローチをしたため解釈の余地、及び至らぬ点があるかと思います。
- 「私は~と思う」「~な処理の仕方もあるんじゃない?」など、忌憚ないご意見あればコメントいただけると幸いです。