バリデーションをコントローラーに書くと何が問題か
Laravelではコントローラー内で $request->validate([...]) を呼ぶことも可能ですが、フォームが複雑になると以下の問題が生じます。
- コントローラーのメソッドが長くなり、単一責任の原則に違反する
- 同じバリデーションロジックを複数のコントローラーで書き直すことになる
- テストが書きにくくなる
FormRequestを使うことで、バリデーションロジックを独立したクラスに分離し、再利用性・テスト性・可読性を高めることができます。
FormRequestの作成
php artisan make:request ContactRequest
生成されたクラス(app/Http/Requests/ContactRequest.php)を編集します。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ContactRequest extends FormRequest
{
/**
* このリクエストを実行できるか(認証チェック)
*/
public function authorize(): bool
{
return true; // 全ユーザーに許可
}
/**
* バリデーションルール
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email:rfc,dns', 'max:255'],
'phone' => ['nullable', 'regex:/^[0-9\-\+]{10,15}$/'],
'subject' => ['required', 'string', 'max:200'],
'body' => ['required', 'string', 'min:10', 'max:5000'],
'agreed' => ['required', 'accepted'],
];
}
/**
* 日本語エラーメッセージ
*/
public function messages(): array
{
return [
'name.required' => 'お名前は必須です',
'name.max' => 'お名前は100文字以内で入力してください',
'email.required' => 'メールアドレスは必須です',
'email.email' => '有効なメールアドレスを入力してください',
'phone.regex' => '電話番号は半角数字・ハイフンで入力してください',
'subject.required' => '件名は必須です',
'body.required' => 'お問い合わせ内容は必須です',
'body.min' => 'お問い合わせ内容は10文字以上入力してください',
'agreed.accepted' => 'プライバシーポリシーへの同意が必要です',
];
}
/**
* 属性名の日本語化
*/
public function attributes(): array
{
return [
'name' => 'お名前',
'email' => 'メールアドレス',
'phone' => '電話番号',
'subject' => '件名',
'body' => 'お問い合わせ内容',
];
}
}
コントローラーはシンプルになる
FormRequestを使うと、コントローラーは型ヒントを指定するだけで自動的にバリデーションが実行されます。
class ContactController extends Controller
{
public function store(ContactRequest $request): RedirectResponse
{
// ここに来た時点でバリデーション済み
$validated = $request->validated();
Contact::create($validated);
// メール送信
Mail::to(config('mail.admin_address'))
->send(new ContactReceived($validated));
return redirect()->route('contact.complete')
->with('success', 'お問い合わせを受け付けました');
}
}
カスタムバリデーションルール
独自のビジネスロジックに基づくバリデーションはカスタムルールとして定義します。
php artisan make:rule JapanesePhone
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class JapanesePhone implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
// 日本の固定電話・携帯・フリーダイヤルに対応
if (!preg_match('/^(0[0-9]{1,4}-[0-9]{1,4}-[0-9]{4}|0[789]0-[0-9]{4}-[0-9]{4})$/', $value)) {
$fail(':attributeの形式が正しくありません(例:03-1234-5678)');
}
}
}
// FormRequestで使用
'phone' => ['nullable', new JapanesePhone],
条件付きバリデーション
状況に応じてルールを変えたい場合は sometimes や when を使います。
public function rules(): array
{
return [
'type' => ['required', Rule::in(['personal', 'corporate'])],
'company' => [
// type が corporate のときのみ必須
Rule::when($this->input('type') === 'corporate', ['required', 'string', 'max:100']),
],
'tax_id' => [
Rule::when($this->input('type') === 'corporate', ['required', 'regex:/^[0-9]{13}$/']),
],
];
}
バリデーション後の処理フック
prepareForValidation でバリデーション前にデータを整形できます。
protected function prepareForValidation(): void
{
// 全角スペースを半角に変換、前後の空白を除去
$this->merge([
'name' => trim(mb_convert_kana($this->name ?? '', 's')),
'email' => strtolower(trim($this->email ?? '')),
'phone' => preg_replace('/[^\d\-]/', '', $this->phone ?? ''),
]);
}
テスト
FormRequestはユニットテストが書きやすいのも利点です。
class ContactRequestTest extends TestCase
{
use RefreshDatabase;
public function test_有効なリクエストはバリデーションを通過する(): void
{
$request = ContactRequest::create('/contact', 'POST', [
'name' => '山田太郎',
'email' => 'yamada@example.com',
'subject' => 'お見積り相談',
'body' => 'Webシステムの開発についてご相談したいです。',
'agreed' => '1',
]);
$validator = Validator::make($request->all(), (new ContactRequest)->rules());
$this->assertFalse($validator->fails());
}
public function test_メールアドレスが不正な場合はバリデーションエラー(): void
{
$validator = Validator::make(
['email' => 'invalid-email'],
['email' => ['required', 'email:rfc,dns']]
);
$this->assertTrue($validator->fails());
}
}
まとめ
FormRequestを使うことで、バリデーションロジックの分離・再利用・テストが容易になります。カスタムルール・条件付きバリデーション・前処理フックを組み合わせることで、複雑なビジネスロジックにも柔軟に対応できます。
LaravelによるWebシステム開発のご相談はお気軽にどうぞ。