LaravelのFormRequest
を継承した子クラスでは、認可やバリデーションを行います。
その後のコントローラーでは、リクエストデータを取得・加工し、最終的にDB(データベース)に保存することが一般的です。
本ページでは、バリデーション後のコントローラー内でのデータの取り扱いについて考えます。
改めて箇条書きにしてみましょう。
- データ取得
- データの加工
- DBへの保存
最終行程でDBへの保存がある場合、データの取り扱いは特に重要と言えます。
$request->validated();
Laravelの従来からある方法がRequest
クラスのvalidated()
メソッドを利用する方法です。
データの取得
その名の通り、バリデーションを通過したデータのみを取得します。
これにより、無効なデータをコントローラー内で誤って使用することを防ぎます。
public function store(UserRequest $request)
{
$validated = $request->validated();
// $validatedの内容
// array:4 [
// "name" => "山田ジョン"
// "email" => "john@example.com"
// "tel" => "03-0000-0000"
// "agreed_to_terms" => 1
// ]
User::create($validated);
}
データの加工
先ほどのコードで「規約に同意する」チェックボックスを想定するagreed_to_terms
のデータがあります。
これはバリデーションには使うけれど、DBには保存する必要が無いとします。以外とこういったデータはありますよね。
加えて「メールアドレス認証が済んでいるフラグ」を追加したい、そんな場合の加工例です。
public function store(UserRequest $request)
{
$validated = $request->validated();
// "agreed_to_terms" 項目を削除
unset($validated['agreed_to_terms']);
// メールアドレス認証が済んでいるフラグを追加
$validated['email_verified'] = 1;
// $validatedの内容
// array:4 [
// "name" => "山田ジョン"
// "email" => "john@example.com"
// "tel" => "03-0000-0000"
// "email_verified" => 1
// ]
User::create($validated);
}
意図としては分かりますし、特に問題は感じません。ですが配列になっているので、複雑な処理を行う場合は見づらくなることもあります。
配列をコレクションに変換する方法もあり、時により直感的なコードになります。
public function store(UserRequest $request)
{
$validated = collect($request->validated())
->forget('agreed_to_terms')
->put('email_verified', 1);
// $validatedの内容
// array:4 [
// "name" => "山田ジョン"
// "email" => "john@example.com"
// "tel" => "03-0000-0000"
// "email_verified" => 1
// ]
// コレクションを配列に戻してDBに保存
User::create($validated->toArray());
}
->
でつなぐチェーンメソッド記法で記述できるため、(Laravelに慣れていれば)分かりやすくなりました。
この例ではそれほどメリットは大きくありませんが、場合によってはすっきり書くことができます。
$request->safe()
Laravel8の途中から、$request->safe()
が追加されました。こちらも同様にsafe(安全)なデータの取得・加工に利用できます。
$request->safe()
は、lluminate\Support\ValidatedInput
のインスタンスを返します。
lluminate\Support\ValidatedInput
は、次の3つの代表的なデータ取得メソッドを持ちます。
- all()
- only()
- except()
データの取得
まずはall()
から見ていきましょう。
public function store(UserRequest $request)
{
$validated = $request->safe()->all();
// $validatedの内容
// array:4 [
// "name" => "山田ジョン"
// "email" => "john@example.com"
// "tel" => "03-0000-0000"
// "agreed_to_terms" => 1
// ]
$request->validated()
と同様、配列でバリデーション済みのデータを取得できます。
$request->validated()
との違いは疑問が生じるところです。……が、マニュアルには深く記載されておらず、コードを読み解く必要があります。
これについては時間があるときにでも調べてみたいところです。
続いてonly()
メソッドとexcept()
メソッドです。その名の通り、「だけ(only)」「除く(except)」したいものを配列で指定します。
public function store(UserRequest $request)
{
// "name", "email"だけ欲しい
$validated = $request->safe()->only(['name', 'email']);
// $validatedの内容
// array:2 [
// "name" => "山田ジョン"
// "email" => "john@example.com"
// ]
// "agreed_to_terms"を除いて全部
$validated = $request->safe()->except(['agreed_to_terms']);
// $validatedの内容
// array:3 [
// "name" => "山田ジョン"
// "email" => "john@example.com"
// "tel" => "03-0000-0000"
// ]
分かりやすいですね。いずれも配列で結果が返ってきます。
データの加工
all()
, only()
, except()
はそれぞれ配列を返すので、これまでにご紹介した方法で操作は可能です。
しかしながらここでもコレクションにして操作してみます。
public function store(UserRequest $request)
{
$validated = collect($request->safe()->all())
->forget('agreed_to_terms')
->put('email_verified', 1);
// $validatedの内容
// Collection:4 [
// "name" => "山田ジョン"
// "email" => "john@example.com"
// "tel" => "03-0000-0000"
// "email_verified" => 1
// ]
// コレクションを配列に戻してDBに保存
User::create($validated->toArray());
}
lluminate\Support\ValidatedInput
のインスタンスを返してくれるメソッドがあれば、メソッドチェーン処理できるのにいいのになぁと思う所です。
その他
現実問題として、バリデーション後にも以下が使われているコードも目にします。
- $request->name
- $request->input(‘name’)
たしかにこれらでも、リクエストデータを取得することが可能です。
しかし、わざわざバリデーション後の専用のデータ取得メソッドがあるわけなので使わない方がよいハズ……。というわけで、これらがダメな理由を考えてみましょう。
加工後の値は取れる?
まずは$request->input()
=入力値なわけですから、以下の様にFormRequest
内で、prepareForValidation()
で加工した場合、input()等で加工後の値は取れるんだっけ!?と考えました。
protected function prepareForValidation()
{
parent::prepareForValidation();
// 入力データ取得
$tel = $this->input('tel');
// ハイフンに似た文字を半角ハイフンに変換(この処理はちゃんとやるともっと複雑)
$tel = str_replace(['-', '―', '‐', 'ー'], '-', $tel);
// 半角数字を全角数字に変換
$tel = mb_convert_kana($tel, 'n');
// 変換後のtelをリクエストデータにセット
$this->merge(['tel' => $tel]);
}
実験してみたところ、これは問題無く加工後のデータを取得可能でした。
merge()
してしまっていますからね……。でもこれをinput()
で取得するのは直感的ではないような気もします。
意図しないリクエストデータの混入
inputはユーザーのリクエストデータ全てを取得してしまうので、問題があるデータが含まれる可能性があります。
例えば今回の例では、_tokenまで含まれてしまっています。
public function store(UserRequest $request)
{
$validated = $request->input();
// $validatedの内容
// array:5 [
// "_token" => "ZM1IzTjNPK96eiQZe4MKTGcOA4qil9IayHRSn9bU"
// "name" => "山田ジョン"
// "email" => "john@example.com"
// "tel" => "03-0000-0000"
// "agreed_to_terms" => 1
// ]
User::create($validated);
_tokenはLaravelの機能なので置いておきますが、もしもこれが攻撃に利用されると問題です。
以下のようにリクエストされた場合を考えます。
public function store(UserRequest $request)
{
$validated = $request->input();
// $validatedの内容
// array:5 [
// "_token" => "ZM1IzTjNPK96eiQZe4MKTGcOA4qil9IayHRSn9bU"
// "name" => "山田ジョン"
// "email" => "john@example.com"
// "tel" => "03-0000-0000"
// "agreed_to_terms" => 1
// "admin_flag" => 1 // 悪意を持ったユーザーが送信する可能性がある
// ]
User::create($validated);
この例では、攻撃者がadmin_flag
のような追加の項目を送信した場合、それがそのままデータベースに保存されるリスクがあります。
このような操作をMass Assignment Vulnerability(大量代入の脆弱性)と呼びます。
Laravelでは、モデルの$fillable
や$guarded
を適切に設定することで、この問題を対処することができます。
……が、リクエストデータを取得する時点で気をつけておくにこしたことはありません。そもそも、意図しないデータをモデルには渡したくないですよね。
結論
Laravelには$request->validated()
や$request->safe()
などの専用のメソッドが用意されています。
それらは名称からしてはっきり意図が分かります。第三者がコードを読んでも一瞬で分かる=その方が良いコードと言えるでしょう。
加えてセキュリティ的な問題も回避できます。
総合的に考えるとFormRequest
によるバリデーション後のコントローラー内での処理は、特に理由が無い限りそれらを使った方が良いと考えます。
まとめ
FormRequest
でバリデーションをした後DBに保存するのは、Webアプリケーションでは頻繁に行われます。
例えDBに保存せずとも、正しい知識を持ってデータの取得&加工を行いたいものですね。
また、モデルの$fillable, $guardedなどの知識と組み合わせることでセキュリティリスクを減少させられます。
1つのセキュリティ対策では万全ではありません。LaravelでのWebアプリケーション全体でセキュリティを考えコードを書く必要があります。
コメント