はじめに
Laravel Advent Calendar 2022 の3日目の投稿です。(遅れました...)
関連記事
公式ドキュメント
フォームリクエストとは
フォームリクエスト(FormRequest)は認可(Authorization)と検証(Validation)を行います。
フロントのHTMLフォームから渡ってくる入力データを検証したり、アクセスしたユーザーがそのリクエストを実行する権限があるかを確認できます。
コントローラで行っていた検証や認可の処理をフォームリクエストへ移譲できるので、コントローラの処理をシンプルにできる便利なクラスです。
フォームリクエスト作成
フォームリクエストのテンプレートを作成するコマンドは用意されています。
$ php artisan make:request ArticleStoreRequest
デフォルトでは authorize() と rules() のメソッドが定義されています。
authorize: 認可の判定を行う
rules: バリデーションルールを連想配列で記述する
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
final class ArticleStoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
];
}
}
authorize()
が認可のメソッドで、現在認証済みのユーザーがそのアクションを実行できるか判定します。
失敗すると403エラーレスポンスを返します。
rules()
はバリデーションのメソッドで、リクエストのデータを検証するバリデーションルールを返します。
失敗すると302リダイレクトレスポンスor422エラーレスポンスを返します。
認可処理が不要な場合は authorize()
を true
で返さなくてもメソッドを削除すればokです。
補足: フォームリクエストの命名ルール
私の場合になりますが、命名ルールとしてはシングルアクションコントローラの名前をそのままフォームリクエストに採用します。
- ArticleStoreController => ArticleStoreRequest
補足: GETのフォームリクエストは必要か
HTTPメソッドがGETの場合は、特に検証することはないかもしれませんが認可では使用するので作っておいて良いと思います。
フォームリクエストの認可
final class ArticleStoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return $this->user()->can(Permission::WriteArticles->value);
}
}
記事の書き込み権限のないユーザーはアクセスできない認可処理を行っています。
$this->user()
で認証済みユーザーへアクセスできます。
final class ArticleEditRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return $this->user()->can(Permission::WriteArticles->value) && $this->user()->id === $this->route('article')->user_id;
}
}
$this->route('article')->user_id
Articlesモデルのuser_idフィールドにアクセスしてます。(詳細はルートモデルバインディングの補足へ)
補足: 認可エラーメッセージのカスタマイズ
デフォルトだと This action is unauthorized.
とエラーメッセージが表示されます。
メッセージを変更するには下記のメソッドをオーバーライドしてください。
/**
* Handle a failed authorization attempt.
*
* @return void
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
protected function failedAuthorization(): void
{
throw new AuthorizationException('記事の書き込み権限がありません。');
}
補足: 認可の実装
例では laravel-permission ライブラリを使用した認可処理をしています。
別記事にて紹介しているのでよければ参照ください。
補足: ルートモデルバインディング
Route::get('articles/{article}/edit', [ArticleController::class, 'edit'])->name('articles.edit');
このようにルーティングが定義されている場合
$this->route('article')
すると App\Models\Article
とモデルの名前と一致するのでLaravelのルートモデルバインディング機能によってインスタンスが取得されます。
これが嫌な場合はこうしましょう。
Route::get('articles/{articleId}/edit', [ArticleController::class, 'edit'])->name('articles.edit');
$this->route('articleId')
でモデルのインスタンスではなくURLに入力した内容がそのまま取得できます。
フォームリクエストの検証(Validation)
final class TaskStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'title' => ['required', 'unique:posts', 'max:255'],
// 'title' => 'required|unique:posts|max:255', // パイプライン区切りの文字列でも書ける
];
}
}
バリデーションは |
区切りで書けますが、個人的には配列の方が見やすいので好きです。
配列に対してバリデーションしたい時は .
で繋げます。
補足: 利用可能なバリデーションルール
バリデーションルールはたくさんあります。
公式ドキュメントを参考ください。
補足: 配列のバリデーション
キーなし
$data = [
'article_ids' => [1, 5, 7],
]
public function rules(): array
{
return [
'article_ids' => ['required', 'array'],
'article_ids.*' => ['integer'],
];
}
キーあり
$data = [
'author' => [
'name' => 'ほげほげ',
'description' => 'ぴよぴよ',
],
]
public function rules(): array
{
return [
'author' => ['required', 'array'],
'author.name' => ['required', 'string', 'max:255'],
'author.description' => ['required', 'string'],
];
}
ネストした配列
$data = [
'items' => [
['name' => 'ほげほげ', 'price' => 1000],
['name' => 'ぴよぴよ', 'price' => 2500],
],
]
public function rules(): array
{
return [
'items' => ['required', 'array'],
'items.*.name' => ['required', 'string', 'max:255'],
'items.*.price' => ['required', 'integer'],
];
}
フォームリクエストの使い方
<?php
namespace App\Http\Controllers;
use App\Http\Request\ArticleStoreRequest;
use Illuminate\Http\RedirectResponse;
final class ArticlesController extends Controller
{
/**
* 新しい記事を保存
*
* @param ArticleStoreRequest $request
* @return RedirectResponse
*/
public function store(ArticleStoreRequest $request): RedirectResponse
{
$title = $request->input('title');
// ...
}
}
コントローラのメソッドに引数として定義するとLaravelのメソッドインジェクション機能により、フォームリクエストのインスタンスを受け取ることができます。
コントローラでフォームリクエストのインスタンスを受け取った時点でバリデーション済み、認可済みとなるので
コントローラではコントローラの処理に集中できます。
入力値の受け取り方
色んなメソッドがありますが、主に使用するものを抜粋してご紹介します。
// 入力値をすべて取得
$data = $request->all();
// 指定したフィールドを取得
$title = $request->input('title');
$title = $request->input('title', 'デフォルト値');
$title = $request->title;
// 型を指定して取得
$value = $request->string('some_string_value');
$value = $request->boolean('some_boolean_value');
$value = $request->integer('some_int_value');
$value = $request->float('some_float_value');
// 指定したものを取り出す
$data = $request->only(['title', 'content']);
// 指定したもの以外を取り出す
$data = $request->except(['title', 'content']);
// rules() に定義したフィールドのみ取得
$data = $request->validated();
// rules() に定義したフィールドのみ
$request->safe(); // Illuminate\Support\ValidatedInput のインスタンス
$data = $request->safe()->only(['title', 'content']);
$data = $request->safe()->only(['title', 'content']);
->safe()
は Laravel8.55以降の機能です。
->integer()
, ->float()
は Laravel9.32以降の機能です。
フォームリクエスト毎にエラーメッセージの変更する
エラーメッセージは lang/ja/validation.php
を利用することで日本語化してエラーメッセージを表示できます。
フォームリクエストによっては表示を個別に変更したい場合があります。
final class ArticleStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'title' => ['required', 'unique:posts', 'max:255'],
'author.name' => ['required'],
'author.description' => ['required'],
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'title' => 'タイトル',
'author.name' => '著者名',
'author.description' => '著者詳細',
];
}
/**
* Get custom messages for validator errors.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'title.required' => 'タイトルは必ず入力してください。',
];
}
}
Laravelの標準バリデーションルールのエラーメッセージの多くは :attribute
プレースホルダーを含みます。
attributes()
メソッドをオーバーライドしてカスタム属性名を設定できます。
エラーメッセージそのものを変更したい場合は messages()
メソッドをオーバーライドしてカスタマイズできます。
カスタムした属性名やメッセージは翻訳機能を利用すると表記揺れを防げるのでオススメです。
バリデーション失敗時
FormRequestのバリデーションに失敗した時の動作についてです。
HTTP
バリデーションに失敗すると Illuminate\Validation\ValidationException
の例外が発生し、直前のページへリダイレクトします。
また、バリデーションエラーとリクエストの入力データはセッションに一時保存されています。
Laravel標準の web
ミドルウェアグループの Illuminate\View\Middleware\ShareErrorsFromSession
ミドルウェアによって $errors
変数がすべてのビューで利用可能です。
$errors
変数には Illuminate\Support\MessageBag
インスタンスが入っています。
JetstreamのバリデーションエラーのBladeテンプレートです。
@if ($errors->any())
<div {{ $attributes }}>
<div class="font-medium text-red-600">{{ __('Whoops! Something went wrong.') }}</div>
<ul class="mt-3 list-disc list-inside text-sm text-red-600">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
補足: リダイレクト処理のカスタマイズ
リダイレクトの処理の指定がない場合は一つ前のページにリダイレクトバックします。
リダイレクト処理をカスタムしたい場合は、FormRequest
クラスの getRedirectUrl()
をオーバーライドしてください。
class FormRequest extends Request implements ValidatesWhenResolved
{
/**
* Get the URL to redirect to on a validation error.
*
* @return string
*/
protected function getRedirectUrl()
{
$url = $this->redirector->getUrlGenerator();
if ($this->redirect) {
return $url->to($this->redirect);
} elseif ($this->redirectRoute) {
return $url->route($this->redirectRoute);
} elseif ($this->redirectAction) {
return $url->action($this->redirectAction);
}
return $url->previous();
}
}
下記のプロパティを上書きするとリダイレクト先を変更できるわけですね。
final class ArticleStoreRequest extends FormRequest
{
// protected $redirect = '/'; // パス
// protected $redirectRoute = 'welcome'; // 名前付きルート
// protected $redirectAction = 'WelcomeController@welcome'; // コントローラ
// ...
}
使うことないかも...トリビア〜😇
JSON
Laravelはリクエストヘッダに Accept: application/json
が付いている場合にIlluminate\Validation\ValidationException
の例外が発生が失敗するとが発生するとJSONレスポンスを返却します。
{
"message": "The team name must be a string. (and 4 more errors)",
"errors": {
"team_name": [
"The team name must be a string.",
"The team name must be at least 1 characters."
],
"authorization.role": [
"The selected authorization.role is invalid."
],
"users.0.email": [
"The users.0.email field is required."
],
"users.2.email": [
"The users.2.email must be a valid email address."
]
}
}
さいごに
ルーティングの解説記事を書いてから全機能書いてやるぞと意気込んだもののお蔵入りになっていました笑
アドベントカレンダーの季節になったこともあり重い腰をあげてなんとか書き上げました!