FormRequestの基礎知識と導入メリット
FormRequestとは? コントローラーをスリム化する魔法のクラス
FormRequestは、Laravelが提供する強力なフォームバリデーション機能です。リクエストの検証ロジックをコントローラーから分離し、専用のクラスで管理することができます。これにより、アプリケーションのコードはより整理され、保守性が向上します。
// 従来のコントローラーでのバリデーション public function store(Request $request) { $validated = $request->validate([ 'title' => 'required|max:255', 'body' => 'required', 'published_at' => 'nullable|date', ]); // 以降の処理... } // FormRequestを使用した場合 public function store(PostStoreRequest $request) { // バリデーションは自動的に実行される $post = Post::create($request->validated()); return redirect()->route('posts.show', $post); }
従来のバリデーション処理との比較メリット
FormRequestを使用することで、以下のような明確なメリットが得られます:
観点 | 従来の方法 | FormRequest |
---|---|---|
コードの責務 | コントローラーに集中 | 適切に分離 |
再利用性 | 低い(コピー&ペーストが必要) | 高い(クラスとして再利用可能) |
テスタビリティ | やや難しい | 容易(独立したテストが可能) |
コードの見通し | 複雑になりやすい | クリーンで理解しやすい |
メンテナンス性 | 変更が影響を及ぼしやすい | 影響範囲が限定的 |
FormRequestを使うべき3つのケース
- 複雑なバリデーションルールが必要な場合
- 条件付きバリデーション
- カスタムバリデーションルール
- 複数フィールドの相関チェック
class ComplexFormRequest extends FormRequest { public function rules() { return [ 'email' => 'required|email|unique:users', 'age' => 'required|integer|min:18', 'plan' => 'required|in:basic,premium', 'card_number' => Rule::requiredIf(fn() => $this->plan === 'premium'), ]; } }
- 同じバリデーションルールを複数の場所で使用する場合
- 共通のバリデーションロジック
- アプリケーション全体での一貫性確保
- DRYの原則の実践
class BaseUserRequest extends FormRequest { public function rules() { return [ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users', ]; } } class CreateUserRequest extends BaseUserRequest { public function rules() { return array_merge(parent::rules(), [ 'password' => 'required|min:8|confirmed', ]); } }
- 認可(Authorization)とバリデーションを組み合わせる場合
- ユーザー権限のチェック
- リソースへのアクセス制御
- ビジネスルールの適用
class UpdatePostRequest extends FormRequest { public function authorize() { $post = Post::find($this->route('post')); return $post && $this->user()->can('update', $post); } public function rules() { return [ 'title' => 'required|max:255', 'content' => 'required', 'category_id' => 'exists:categories,id', ]; } }
FormRequestを使用することで、アプリケーションのコードはより構造化され、保守性が向上します。特に大規模なアプリケーションや、複雑なバリデーションロジックを持つケースでは、FormRequestの導入を積極的に検討すべきです。次のセクションでは、FormRequestの具体的な実装手順について詳しく説明していきます。
FormRequestの基本的な手順
artisanコマンドでFormRequestを生成する
FormRequestクラスの作成は、Laravelのartisanコマンドを使用することで簡単に行えます。以下のコマンドを実行することで、必要なボイラープレートコードが自動生成されます。
# 基本的な生成コマンド php artisan make:request StorePostRequest # アプリケーション内の生成場所 # app/Http/Requests/StorePostRequest.php
生成されるファイルの基本構造は以下のようになります:
namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StorePostRequest extends FormRequest { /** * リクエストの認可を判定 */ public function authorize() { return false; // デフォルトはfalse } /** * バリデーションルールを定義 */ public function rules() { return [ // ]; } }
rulesメソッドでバリデーションルールを定義する
rules
メソッドでは、フォームフィールドに対するバリデーションルールを配列形式で定義します。Laravelが提供する豊富なバリデーションルールを活用できます。
public function rules() { return [ // 基本的なルール 'title' => 'required|string|max:255', 'content' => 'required|string|min:10', // 複数のルールを配列で指定 'email' => [ 'required', 'email', 'unique:users,email', ], // クロージャーを使用した動的なルール 'category_id' => [ 'required', 'integer', function ($attribute, $value, $fail) { if (!Category::where('id', $value)->exists()) { $fail('選択されたカテゴリーは存在しません。'); } }, ], // 条件付きルール 'phone' => Rule::when($this->contact_method === 'phone', [ 'required', 'string', 'regex:/^[0-9-]+$/', ]), ]; } // メッセージのカスタマイズ public function messages() { return [ 'title.required' => 'タイトルは必須です。', 'content.min' => '内容は:min文字以上で入力してください。', ]; } // 属性名のカスタマイズ public function attributes() { return [ 'title' => '記事タイトル', 'content' => '記事内容', ]; }
authorizeメソッドで処理を実装する
authorize
メソッドでは、現在のユーザーがリクエストを実行する権限を持っているかどうかを判定します。ここでLaravelのポリシーやゲートと連携することで、きめ細かなアクセス制御が可能になります。
public function authorize() { // 基本的な認可チェック return true; // すべてのユーザーに許可 // ユーザーの権限チェック return $this->user()->can('create', Post::class); // 特定のロールチェック return $this->user()->hasRole('editor'); // 複雑な条件チェック $post = Post::find($this->route('post')); return $post && $this->user()->id === $post->user_id; }
実装のポイント:
- 適切な名前付け
- リクエストクラスの名前は用途を明確に表現する
- 例:
StorePostRequest
,UpdateUserRequest
- バリデーションルールの整理
- 関連するルールをまとめて記述
- 複雑なルールは必要に応じてメソッドに分割
- エラーメッセージのカスタマイズ
- ユーザーフレンドリーなメッセージを設定
- 多言語対応を考慮
- 認可ロジックの適切な実装
- セキュリティを考慮した適切な権限チェック
- ビジネスロジックとの整合性確保
FormRequestの基本的な実装手順を理解することで、クリーンで保守性の高いバリデーション処理を実現できます。次のセクションでは、より実践的なFormRequestの活用テクニックについて説明していきます。
実践的なFormRequestの活用テクニック
カスタムバリデーションルールの追加方法
FormRequestでは、Laravelの標準バリデーションルールに加えて、独自のカスタムルールを実装できます。以下に、よく使用されるパターンを紹介します。
class ProductRequest extends FormRequest { /** * カスタムバリデータの定義 */ public function withValidator($validator) { $validator->addRule('price_range', function ($attribute, $value, $parameters) { return $value >= $parameters[0] && $value <= $parameters[1]; }); // クロージャーによる複雑なバリデーション $validator->after(function ($validator) { if ($this->stock < $this->minimum_stock) { $validator->errors()->add('stock', '在庫数は最小在庫数より大きい必要があります。'); } }); } public function rules() { return [ 'price' => ['required', 'numeric', 'price_range:1000,100000'], 'stock' => 'required|integer|min:0', 'minimum_stock' => 'required|integer|min:0', ]; } }
条件付きバリデーションの実装方法
特定の条件に基づいてバリデーションルールを動的に変更する方法を説明します。
class UpdateUserRequest extends FormRequest { public function rules() { $rules = [ 'name' => 'required|string|max:255', 'email' => [ 'required', 'email', Rule::unique('users')->ignore($this->user->id), ], ]; // ユーザータイプに応じたバリデーション if ($this->input('type') === 'business') { $rules = array_merge($rules, [ 'company_name' => 'required|string|max:255', 'tax_number' => 'required|string|size:13', ]); } // パスワード変更時のみのバリデーション if ($this->filled('password')) { $rules['password'] = ['required', 'min:8', 'confirmed']; $rules['current_password'] = ['required', function ($attribute, $value, $fail) { if (!Hash::check($value, $this->user->password)) { $fail('現在のパスワードが正しくありません。'); } }]; } return $rules; } }
配列データのバリデーション処理
複数の要素を含む配列データに対するバリデーション方法を示します。
class CreateOrderRequest extends FormRequest { public function rules() { return [ // 配列自体のバリデーション 'items' => 'required|array|min:1', // 配列の各要素のバリデーション 'items.*.product_id' => 'required|exists:products,id', 'items.*.quantity' => 'required|integer|min:1', // ネストされた配列のバリデーション 'shipping_addresses' => 'required|array|min:1', 'shipping_addresses.*.postal_code' => 'required|string|size:7', 'shipping_addresses.*.address' => 'required|string|max:255', 'shipping_addresses.*.phone' => 'required|string|regex:/^[0-9-]+$/', ]; } /** * 配列全体に対する追加バリデーション */ public function withValidator($validator) { $validator->after(function ($validator) { $totalQuantity = collect($this->items)->sum('quantity'); if ($totalQuantity > 100) { $validator->errors()->add('items', '注文数量の合計は100を超えることはできません。'); } }); } }
ファイルアップロードのバリデーション設定
ファイルアップロードに特化したバリデーションルールの実装例を紹介します。
class DocumentUploadRequest extends FormRequest { public function rules() { return [ // 単一ファイルのバリデーション 'document' => [ 'required', 'file', 'mimes:pdf,doc,docx', 'max:10240', // 10MB ], // 複数ファイルのバリデーション 'attachments' => 'required|array|min:1|max:5', 'attachments.*' => [ 'required', 'file', 'mimes:jpg,jpeg,png,pdf', 'max:5120', // 5MB ], ]; } /** * ファイルの追加チェック */ public function withValidator($validator) { $validator->after(function ($validator) { if ($this->hasFile('attachments')) { $totalSize = collect($this->file('attachments')) ->sum(function ($file) { return $file->getSize(); }); // 合計サイズチェック(20MB以下) if ($totalSize > 20 * 1024 * 1024) { $validator->errors()->add( 'attachments', '添付ファイルの合計サイズは20MB以下にしてください。' ); } } }); } /** * アップロード前の前処理 */ protected function prepareForValidation() { if ($this->hasFile('document')) { // ファイル名のサニタイズ $this->merge([ 'original_filename' => $this->file('document')->getClientOriginalName() ]); } } }
これらの実践的なテクニックを活用することで、より堅牢で柔軟なバリデーション処理を実装できます。次のセクションでは、FormRequestのエラーハンドリングについて詳しく説明していきます。
FormRequestのエラーハンドリング
エラーメッセージのカスタマイズ方法
FormRequestでは、バリデーションエラーメッセージを細かくカスタマイズすることができます。ユーザーフレンドリーで分かりやすいエラーメッセージを実装する方法を紹介します。
class UserRegistrationRequest extends FormRequest { /** * バリデーションルールの定義 */ public function rules() { return [ 'username' => 'required|string|max:30|unique:users', 'email' => 'required|email|unique:users', 'password' => 'required|min:8|confirmed', 'terms' => 'accepted', ]; } /** * カスタムエラーメッセージの定義 */ public function messages() { return [ 'username.required' => 'ユーザー名を入力してください。', 'username.unique' => 'このユーザー名は既に使用されています。', 'email.email' => '有効なメールアドレスを入力してください。', 'password.min' => 'パスワードは:min文字以上で入力してください。', 'terms.accepted' => '利用規約に同意する必要があります。', ]; } /** * 属性名のカスタマイズ */ public function attributes() { return [ 'username' => 'ユーザー名', 'email' => 'メールアドレス', 'password' => 'パスワード', 'terms' => '利用規約', ]; } /** * エラーメッセージのフォーマット処理 */ protected function formatErrors($validator) { return [ 'status' => 'error', 'message' => '入力内容に誤りがあります。', 'errors' => $validator->errors()->toArray(), ]; } }
バリデーションエラー時のリダイレクト制御
バリデーションエラー発生時のリダイレクト動作をカスタマイズする方法を説明します。
class ProductUpdateRequest extends FormRequest { /** * バリデーションエラー時のリダイレクト先の指定 */ protected $redirectRoute = 'products.edit'; /** * リダイレクト時のパラメータを設定 */ public function withValidator($validator) { if ($validator->fails()) { // セッションにエラー情報を保存 session()->flash('error_type', 'validation'); session()->flash('target_section', 'product_details'); } } /** * カスタムリダイレクタの実装 */ protected function getRedirectUrl() { // デフォルトのリダイレクト先をオーバーライド $url = $this->redirector->getUrlGenerator(); return $url->route('products.edit', [ 'product' => $this->route('product'), 'section' => 'details', ]); } }
APIリクエストエラー応答設定
API用のエラーレスポンスをカスタマイズする方法を示します。
class ApiProductRequest extends FormRequest { /** * リクエストがAPIであることを示す */ public function wantsJson() { return true; } /** * バリデーションエラー時のレスポンス */ protected function failedValidation(Validator $validator) { $response = new JsonResponse([ 'status' => 'error', 'message' => 'バリデーションエラーが発生しました。', 'errors' => $validator->errors(), 'error_code' => 'VALIDATION_ERROR', ], 422); throw new ValidationException($validator, $response); } /** * 認可エラー時のレスポンス */ protected function failedAuthorization() { throw new HttpResponseException( response()->json([ 'status' => 'error', 'message' => 'この操作を実行する権限がありません。', 'error_code' => 'UNAUTHORIZED', ], 403) ); } /** * グローバルなエラーハンドリング */ public function withValidator($validator) { $validator->after(function ($validator) { try { // ビジネスロジックのバリデーション $this->validateBusinessRules(); } catch (BusinessRuleException $e) { $validator->errors()->add('business_rule', $e->getMessage()); } }); } /** * エラーレスポンスのフォーマット */ public function formatErrors(Validator $validator) { return [ 'status' => 'error', 'errors' => $validator->errors()->toArray(), 'debug_id' => uniqid('val_'), 'timestamp' => now()->toIso8601String(), ]; } } // APIエラーレスポンスの使用例 class ApiErrorHandler extends Handler { protected function invalidJson($request, ValidationException $exception) { return response()->json([ 'message' => $exception->getMessage(), 'errors' => $this->transformErrors($exception), ], $exception->status); } private function transformErrors(ValidationException $exception) { $errors = []; foreach ($exception->errors() as $field => $message) { $errors[] = [ 'field' => $field, 'message' => $message[0], 'code' => 'INVALID_' . strtoupper($field), ]; } return $errors; } }
FormRequestのエラーハンドリングを適切に実装することで、ユーザーフレンドリーなエラー通知とAPI応答を実現できます。エラーメッセージの多言語化や、エラーログの記録なども考慮に入れることで、より堅牢なアプリケーションを構築できます。次のセクションでは、FormRequestのテスト実装について説明していきます。
FormRequestのテスト実装
FormRequest専用のユニットテストの書き方
FormRequestのテストは、バリデーションルールとビジネスロジックが正しく機能することを確認する重要な工程です。以下に、効果的なテスト実装の方法を示します。
namespace Tests\Unit\Http\Requests; use Tests\TestCase; use App\Http\Requests\CreateProductRequest; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; class CreateProductRequestTest extends TestCase { private CreateProductRequest $request; protected function setUp(): void { parent::setUp(); $this->request = new CreateProductRequest(); } /** * バリデーションルールのテスト */ public function test_validation_rules() { // 期待されるルールの定義を確認 $rules = $this->request->rules(); $this->assertEquals([ 'name' => ['required', 'string', 'max:255'], 'price' => ['required', 'numeric', 'min:0'], 'description' => ['nullable', 'string'], ], $rules); } /** * 正常系のバリデーションテスト */ public function test_passes_valid_data() { $validator = Validator::make([ 'name' => '商品名', 'price' => 1000, 'description' => '商品の説明', ], $this->request->rules()); $this->assertFalse($validator->fails()); } /** * 異常系のバリデーションテスト */ public function test_fails_invalid_data() { $validator = Validator::make([ 'name' => '', // required違反 'price' => -100, // min:0違反 ], $this->request->rules()); $this->assertTrue($validator->fails()); $this->assertArrayHasKey('name', $validator->errors()->toArray()); $this->assertArrayHasKey('price', $validator->errors()->toArray()); } /** * カスタムバリデーションルールのテスト */ public function test_custom_validation_rules() { $request = $this->createRequest([ 'name' => '商品名', 'price' => 1000, 'stock' => 5, 'minimum_stock' => 10, ]); $validator = Validator::make( $request->all(), $request->rules() ); $validator->after($request->withValidator($validator)); $this->assertTrue($validator->fails()); $this->assertArrayHasKey('stock', $validator->errors()->toArray()); } /** * 認可ロジックのテスト */ public function test_authorization() { $user = $this->createUser(['role' => 'admin']); $this->actingAs($user); $this->assertTrue($this->request->authorize()); $user = $this->createUser(['role' => 'guest']); $this->actingAs($user); $this->assertFalse($this->request->authorize()); } }
テスト時によく発生するエラーとその解決方法
FormRequestのテスト実装時によく遭遇する問題とその解決方法を紹介します。
- モックの活用
class ProductRequestTest extends TestCase { public function test_unique_validation_with_mock() { // DBアクセスをモック化 $this->mock(ProductRepository::class, function ($mock) { $mock->shouldReceive('findByName') ->with('既存商品') ->andReturn(true); }); $validator = Validator::make([ 'name' => '既存商品' ], $this->request->rules()); $this->assertTrue($validator->fails()); } }
- テストヘルパーの作成
trait FormRequestTestHelper { protected function assertRequestValidationFails($requestClass, $data) { $request = new $requestClass(); $validator = Validator::make($data, $request->rules()); $this->assertTrue($validator->fails()); return $validator->errors(); } protected function assertRequestValidationPasses($requestClass, $data) { $request = new $requestClass(); $validator = Validator::make($data, $request->rules()); $this->assertFalse($validator->fails()); } }
- 認証・認可のテスト
class UpdateProductRequestTest extends TestCase { public function test_authorization_with_different_roles() { $testCases = [ 'admin' => true, 'manager' => true, 'user' => false, ]; foreach ($testCases as $role => $expectedResult) { $user = User::factory()->create(['role' => $role]); $this->actingAs($user); $request = new UpdateProductRequest(); $this->assertEquals($expectedResult, $request->authorize()); } } }
- エラーメッセージのテスト
class UserRequestTest extends TestCase { public function test_custom_error_messages() { $request = new CreateUserRequest(); $validator = Validator::make([ 'email' => 'invalid-email', ], $request->rules(), $request->messages()); $errors = $validator->errors(); $this->assertEquals( 'メールアドレスの形式が正しくありません。', $errors->first('email') ); } }
FormRequestのテストを適切に実装することで、バリデーションロジックの信頼性を確保し、予期せぬバグの早期発見が可能になります。次のセクションでは、FormRequestのベストプラクティスとヒントについて説明していきます。
FormRequestのベストプラクティスとヒント
FormRequestの共通処理を親クラスに実装する
大規模なアプリケーションでは、FormRequest間で共通する処理を親クラスに実装することで、コードの重複を避け、保守性を向上させることができます。
abstract class BaseFormRequest extends FormRequest { /** * 共通のバリデーションルール */ protected function baseRules(): array { return [ 'created_by' => 'exists:users,id', 'updated_by' => 'exists:users,id', ]; } /** * 共通のエラーメッセージ */ protected function baseMessages(): array { return [ 'required' => ':attributeは必須項目です。', 'string' => ':attributeは文字列で入力してください。', 'max' => ':attributeは:max文字以内で入力してください。', ]; } /** * 共通の認可ロジック */ public function authorize(): bool { return $this->user() !== null; } /** * グローバルなバリデーション前処理 */ protected function prepareForValidation() { $this->merge([ 'updated_by' => $this->user()->id, ]); } /** * 共通のバリデーションエラーハンドリング */ protected function failedValidation(Validator $validator) { // ログ出力 Log::warning('Validation failed', [ 'request' => $this->route()->getName(), 'errors' => $validator->errors()->toArray(), 'input' => $this->except(['password']), ]); parent::failedValidation($validator); } } // 具体的な実装例 class CreateProductRequest extends BaseFormRequest { public function rules(): array { return array_merge($this->baseRules(), [ 'name' => 'required|string|max:255', 'price' => 'required|numeric|min:0', ]); } public function messages(): array { return array_merge($this->baseMessages(), [ 'price.min' => '価格は0以上で入力してください。', ]); } }
バリデーションルールの再利用とDRY原則の実践
バリデーションルールを再利用可能なコンポーネントとして設計することで、保守性と一貫性を向上させることができます。
// バリデーションルールのトレイト trait HasAddressValidation { protected function addressRules(): array { return [ 'postal_code' => ['required', 'string', 'size:7'], 'prefecture' => ['required', 'string', Rule::in(config('prefectures'))], 'city' => ['required', 'string', 'max:255'], 'street' => ['required', 'string', 'max:255'], 'building' => ['nullable', 'string', 'max:255'], ]; } } trait HasContactValidation { protected function contactRules(): array { return [ 'email' => ['required', 'email', 'max:255'], 'phone' => ['required', 'string', 'regex:/^[0-9-]{10,11}$/'], ]; } } // ルールセットの作成 class ValidationRuleSets { public static function passwordRules(): array { return [ 'required', 'string', 'min:8', 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/', ]; } public static function imageRules(int $maxSize = 5120): array { return [ 'required', 'image', 'mimes:jpeg,png,jpg', "max:{$maxSize}", ]; } } // 実際のFormRequestでの使用 class CustomerRegistrationRequest extends BaseFormRequest { use HasAddressValidation, HasContactValidation; public function rules(): array { return array_merge( $this->addressRules(), $this->contactRules(), [ 'name' => ['required', 'string', 'max:255'], 'password' => ValidationRuleSets::passwordRules(), 'avatar' => ValidationRuleSets::imageRules(), ] ); } }
大規模アプリケーションでのFormRequest設計パターン
大規模アプリケーションでは、FormRequestを効率的に管理するための設計パターンが重要になります。
// ドメイン別のFormRequestの基底クラス namespace App\Http\Requests\User; abstract class UserBaseRequest extends BaseFormRequest { protected function userRules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', Rule::unique('users')->ignore($this->user)], ]; } } // フォームリクエストファクトリー class FormRequestFactory { public static function create(string $type, array $data = []): FormRequest { return match ($type) { 'user.create' => new CreateUserRequest($data), 'user.update' => new UpdateUserRequest($data), 'product.create' => new CreateProductRequest($data), default => throw new InvalidArgumentException('Unknown request type'), }; } } // バリデーションサービス class ValidationService { public function validateWithRules(array $data, array $rules): array { return Validator::make($data, $rules)->validate(); } public function validateWithRequest(string $requestType, array $data): array { $request = FormRequestFactory::create($requestType, $data); return $this->validateWithRules($data, $request->rules()); } } // ビジネスルールのバリデータ class BusinessRuleValidator { public function validateInventory(Product $product, int $quantity): bool { return $product->stock >= $quantity; } public function validateUserSubscription(User $user, string $feature): bool { return $user->subscription->hasFeature($feature); } } // FormRequestでの使用例 class CreateOrderRequest extends BaseFormRequest { private BusinessRuleValidator $validator; public function __construct(BusinessRuleValidator $validator) { $this->validator = $validator; } public function withValidator($validator) { $validator->after(function ($validator) { $product = Product::find($this->input('product_id')); if (!$this->validator->validateInventory($product, $this->input('quantity'))) { $validator->errors()->add('quantity', '在庫が不足しています。'); } if (!$this->validator->validateUserSubscription($this->user(), 'create_order')) { $validator->errors()->add('subscription', 'この機能を利用するには、サブスクリプションのアップグレードが必要です。'); } }); } }
これらのベストプラクティスを活用することで、保守性が高く、スケーラブルなFormRequestの実装が可能になります。特に大規模なアプリケーションでは、これらのパターンを適切に組み合わせることで、効率的なコード管理と開発が実現できます。