Laravel Validation

バリデーションライブラリPHPLaravelフォームリクエストカスタムバリデーターサーバーサイド

GitHub概要

laravel/framework

The Laravel Framework.

スター33,844
ウォッチ958
フォーク11,444
作成日:2013年1月10日
言語:PHP
ライセンス:MIT License

トピックス

frameworklaravelphp

スター履歴

laravel/framework Star History
データ取得日時: 2025/7/19 09:31

ライブラリ

Laravel Validation

概要

Laravel Validationは、LaravelフレームワークにビルトインされたPHP向けの包括的なバリデーションシステムです。豊富な組み込みルール、フォームリクエスト統合、カスタムバリデーターのサポートにより、シンプルな入力検証から複雑なビジネスロジックまで対応可能。MVCアーキテクチャと完全に統合され、コントローラーを清潔に保ちながら堅牢なデータ検証を実現します。2025年現在、PHPウェブ開発における最も人気のあるバリデーションソリューションの一つです。

詳細

Laravel 11.xおよび12.xは2025年現在の最新版で、世界中の数百万のプロジェクトで使用されているPHPエコシステムの標準的なバリデーションシステムです。60以上の組み込みバリデーションルールを提供し、必須フィールド、データ型、フォーマット、サイズ、データベース検証など幅広い検証ニーズに対応。フォームリクエストクラスによる検証ロジックのカプセル化、カスタムルールオブジェクトの作成、条件付きバリデーション、配列とネストデータの検証など、エンタープライズレベルの要件に対応する高度な機能を備えています。

主な特徴

  • 60以上の組み込みルール: required、email、unique、exists、minなど豊富なバリデーションルール
  • フォームリクエスト統合: 検証ロジックを専用クラスにカプセル化し、コントローラーを清潔に保つ
  • カスタムバリデーター: クロージャ、ルールオブジェクト、バリデーター拡張による柔軟なカスタマイズ
  • 自動エラーハンドリング: Webリクエストでは自動リダイレクト、APIでは422 JSONレスポンス
  • 多言語対応: エラーメッセージの国際化と簡単なカスタマイズ
  • 条件付きバリデーション: 動的なルール適用とコンテキストベースの検証

メリット・デメリット

メリット

  • Laravelフレームワークとの完全な統合による開発効率の向上
  • 豊富な組み込みルールセットによる即座の生産性
  • フォームリクエストによるクリーンなコード構造の実現
  • 自動エラーハンドリングとリダイレクト機能
  • 日本語を含む多言語対応の充実したサポート
  • アクティブなコミュニティと豊富なドキュメント

デメリット

  • Laravelフレームワーク外での使用が困難(スタンドアロン利用に制限)
  • 学習曲線が急(多くの機能と規約の理解が必要)
  • 小規模プロジェクトではオーバーエンジニアリングになる可能性
  • パフォーマンスオーバーヘッド(特に大量のデータ検証時)
  • カスタマイズが複雑になりがちな場合がある
  • フレームワークのバージョンアップ時の互換性問題

参考ページ

書き方の例

基本的なバリデーション実装

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class UserController extends Controller
{
    /**
     * 基本的なバリデーション例
     */
    public function store(Request $request)
    {
        // シンプルなバリデーション
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8|confirmed',
            'age' => 'nullable|integer|min:18|max:100',
            'terms' => 'accepted'
        ]);

        // バリデーション成功後の処理
        // $validatedには検証済みデータのみが含まれる
        User::create($validated);

        return redirect('/users')->with('success', 'ユーザーが作成されました');
    }

    /**
     * 配列形式でのルール定義
     */
    public function update(Request $request, $id)
    {
        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => [
                'required',
                'email',
                Rule::unique('users')->ignore($id)
            ],
            'profile' => ['nullable', 'array'],
            'profile.bio' => ['nullable', 'string', 'max:1000'],
            'profile.website' => ['nullable', 'url'],
            'skills' => ['required', 'array', 'min:1'],
            'skills.*' => ['required', 'string', 'distinct']
        ]);

        User::findOrFail($id)->update($validated);

        return redirect()->back()->with('success', '更新が完了しました');
    }
}

// カスタムエラーメッセージの定義
public function storeWithCustomMessages(Request $request)
{
    $messages = [
        'name.required' => '名前は必須項目です',
        'email.required' => 'メールアドレスを入力してください',
        'email.email' => '有効なメールアドレスを入力してください',
        'email.unique' => 'このメールアドレスは既に使用されています',
        'password.min' => 'パスワードは:min文字以上で入力してください',
        'password.confirmed' => 'パスワードが確認用と一致しません'
    ];

    $attributes = [
        'name' => '名前',
        'email' => 'メールアドレス',
        'password' => 'パスワード'
    ];

    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users',
        'password' => 'required|min:8|confirmed'
    ], $messages, $attributes);

    // 処理を継続...
}

フォームリクエストの実装

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class StoreUserRequest extends FormRequest
{
    /**
     * ユーザーがこのリクエストを実行する権限があるかを判定
     */
    public function authorize(): bool
    {
        // 認証されたユーザーのみ許可
        return auth()->check();
    }

    /**
     * バリデーションルールの定義
     */
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'unique:users,email'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
            'role' => ['required', Rule::in(['admin', 'user', 'moderator'])],
            'profile_image' => ['nullable', 'image', 'max:2048'], // 2MBまで
            'birth_date' => ['nullable', 'date', 'before:today'],
            'phone' => ['nullable', 'regex:/^0[0-9]{9,10}$/'], // 日本の電話番号
            'postal_code' => ['nullable', 'regex:/^\d{3}-?\d{4}$/'], // 日本の郵便番号
            'preferences' => ['nullable', 'array'],
            'preferences.notifications' => ['boolean'],
            'preferences.newsletter' => ['boolean']
        ];
    }

    /**
     * カスタムエラーメッセージ
     */
    public function messages(): array
    {
        return [
            'name.required' => '名前を入力してください',
            'name.max' => '名前は255文字以内で入力してください',
            'email.required' => 'メールアドレスを入力してください',
            'email.email' => '有効なメールアドレスを入力してください',
            'email.unique' => 'このメールアドレスは既に登録されています',
            'password.required' => 'パスワードを入力してください',
            'password.min' => 'パスワードは8文字以上で入力してください',
            'password.confirmed' => 'パスワードが確認用と一致しません',
            'role.required' => '役割を選択してください',
            'role.in' => '無効な役割が選択されました',
            'profile_image.image' => '画像ファイルをアップロードしてください',
            'profile_image.max' => '画像は2MB以下にしてください',
            'birth_date.date' => '有効な日付を入力してください',
            'birth_date.before' => '誕生日は今日より前の日付を入力してください',
            'phone.regex' => '有効な電話番号を入力してください(例:090-1234-5678)',
            'postal_code.regex' => '有効な郵便番号を入力してください(例:123-4567)'
        ];
    }

    /**
     * カスタム属性名
     */
    public function attributes(): array
    {
        return [
            'name' => '名前',
            'email' => 'メールアドレス',
            'password' => 'パスワード',
            'role' => '役割',
            'profile_image' => 'プロフィール画像',
            'birth_date' => '誕生日',
            'phone' => '電話番号',
            'postal_code' => '郵便番号'
        ];
    }

    /**
     * バリデーション前のデータ準備
     */
    protected function prepareForValidation(): void
    {
        // 電話番号からハイフンを削除
        if ($this->has('phone')) {
            $this->merge([
                'phone' => str_replace('-', '', $this->phone)
            ]);
        }

        // メールアドレスを小文字に変換
        if ($this->has('email')) {
            $this->merge([
                'email' => strtolower($this->email)
            ]);
        }
    }

    /**
     * バリデーション後の処理
     */
    protected function passedValidation(): void
    {
        // パスワードをハッシュ化
        $this->merge([
            'password' => bcrypt($this->password)
        ]);
    }
}

// コントローラーでの使用
namespace App\Http\Controllers;

use App\Http\Requests\StoreUserRequest;
use App\Models\User;

class UserController extends Controller
{
    public function store(StoreUserRequest $request)
    {
        // $requestは既にバリデーション済み
        $user = User::create($request->validated());

        return redirect()->route('users.show', $user)
            ->with('success', 'ユーザーが正常に作成されました');
    }
}

カスタムバリデーションルールの作成

<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class JapanesePhoneNumber implements ValidationRule
{
    /**
     * バリデーションを実行
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // 日本の電話番号形式をチェック
        $patterns = [
            '/^0\d{9,10}$/',           // 固定電話(ハイフンなし)
            '/^0\d{1,4}-\d{1,4}-\d{4}$/', // 固定電話(ハイフンあり)
            '/^0[789]0-\d{4}-\d{4}$/',    // 携帯電話(ハイフンあり)
            '/^0[789]0\d{8}$/'             // 携帯電話(ハイフンなし)
        ];

        $isValid = false;
        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $value)) {
                $isValid = true;
                break;
            }
        }

        if (!$isValid) {
            $fail('有効な日本の電話番号を入力してください');
        }
    }
}

// 複雑なビジネスロジックを含むカスタムルール
namespace App\Rules;

use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;

class ValidBusinessHours implements ValidationRule, DataAwareRule
{
    /**
     * バリデーション対象の全データ
     */
    protected array $data = [];

    /**
     * バリデーション対象の全データを設定
     */
    public function setData(array $data): static
    {
        $this->data = $data;
        return $this;
    }

    /**
     * 営業時間のバリデーション
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // 開始時間と終了時間の取得
        $openTime = $this->data['open_time'] ?? null;
        $closeTime = $this->data['close_time'] ?? null;

        if (!$openTime || !$closeTime) {
            $fail('営業時間を正しく入力してください');
            return;
        }

        // 時間の妥当性チェック
        $open = strtotime($openTime);
        $close = strtotime($closeTime);

        if ($open >= $close) {
            $fail('閉店時間は開店時間より後に設定してください');
            return;
        }

        // 営業時間の長さチェック(最大16時間)
        if (($close - $open) > (16 * 3600)) {
            $fail('営業時間は16時間以内に設定してください');
            return;
        }

        // 深夜営業のチェック
        $closeHour = (int)date('H', $close);
        if ($closeHour >= 2 && $closeHour <= 5) {
            if (!($this->data['has_late_night_permit'] ?? false)) {
                $fail('深夜営業には許可が必要です');
            }
        }
    }
}

// カスタムルールの使用例
public function storeShop(Request $request)
{
    $validated = $request->validate([
        'name' => ['required', 'string', 'max:255'],
        'phone' => ['required', new JapanesePhoneNumber],
        'open_time' => ['required', 'date_format:H:i'],
        'close_time' => ['required', 'date_format:H:i', new ValidBusinessHours],
        'has_late_night_permit' => ['boolean']
    ]);

    Shop::create($validated);

    return redirect()->route('shops.index')
        ->with('success', '店舗が登録されました');
}

高度なバリデーション技法

<?php

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

class AdvancedValidationController extends Controller
{
    /**
     * 条件付きバリデーション
     */
    public function conditionalValidation(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'account_type' => 'required|in:personal,business',
            'name' => 'required|string|max:255',
            'email' => 'required|email'
        ]);

        // アカウントタイプに応じて追加ルール
        $validator->sometimes('company_name', 'required|string|max:255', function ($input) {
            return $input->account_type === 'business';
        });

        $validator->sometimes('company_number', 'required|regex:/^\d{13}$/', function ($input) {
            return $input->account_type === 'business';
        });

        $validator->sometimes('birth_date', 'required|date|before:-18 years', function ($input) {
            return $input->account_type === 'personal';
        });

        if ($validator->fails()) {
            return redirect()->back()
                ->withErrors($validator)
                ->withInput();
        }

        // 処理を継続...
    }

    /**
     * 複雑な配列バリデーション
     */
    public function arrayValidation(Request $request)
    {
        $validated = $request->validate([
            'products' => 'required|array|min:1',
            'products.*.name' => 'required|string|max:255',
            'products.*.price' => 'required|numeric|min:0',
            'products.*.quantity' => 'required|integer|min:1',
            'products.*.categories' => 'required|array|min:1',
            'products.*.categories.*' => 'exists:categories,id',
            'products.*.specifications' => 'nullable|array',
            'products.*.specifications.*.key' => 'required_with:products.*.specifications|string',
            'products.*.specifications.*.value' => 'required_with:products.*.specifications|string'
        ]);

        // 在庫チェックなどの追加バリデーション
        $validator = Validator::make($validated, []);
        
        $validator->after(function ($validator) use ($validated) {
            foreach ($validated['products'] as $index => $product) {
                // 在庫チェック
                $stock = Stock::where('product_name', $product['name'])->first();
                if ($stock && $stock->quantity < $product['quantity']) {
                    $validator->errors()->add(
                        "products.{$index}.quantity",
                        "{$product['name']}の在庫が不足しています(在庫: {$stock->quantity})"
                    );
                }
            }
        });

        if ($validator->fails()) {
            return redirect()->back()
                ->withErrors($validator)
                ->withInput();
        }

        // 注文処理...
    }

    /**
     * ファイルアップロードバリデーション
     */
    public function fileUploadValidation(Request $request)
    {
        $validated = $request->validate([
            'document' => [
                'required',
                'file',
                'mimes:pdf,doc,docx',
                'max:10240' // 10MB
            ],
            'images' => [
                'required',
                'array',
                'max:5' // 最大5枚
            ],
            'images.*' => [
                'required',
                'image',
                'mimes:jpeg,png,jpg,webp',
                'max:5120', // 5MB
                'dimensions:min_width=100,min_height=100,max_width=4000,max_height=4000'
            ],
            'video' => [
                'nullable',
                'file',
                'mimetypes:video/mp4,video/mpeg,video/quicktime',
                'max:102400' // 100MB
            ]
        ]);

        // カスタムファイルバリデーション
        if ($request->hasFile('images')) {
            foreach ($request->file('images') as $index => $image) {
                // EXIF情報チェック
                if (function_exists('exif_read_data')) {
                    try {
                        $exif = @exif_read_data($image->getPathname());
                        if ($exif && isset($exif['Orientation'])) {
                            // 画像の向きを修正する処理
                        }
                    } catch (\Exception $e) {
                        // EXIF読み取りエラーの処理
                    }
                }
            }
        }

        // ファイル保存処理...
    }

    /**
     * データベース関連バリデーション
     */
    public function databaseValidation(Request $request)
    {
        $userId = auth()->id();

        $validated = $request->validate([
            // ユニーク制約(自分自身を除外)
            'email' => [
                'required',
                'email',
                Rule::unique('users')->ignore($userId)
            ],
            
            // 存在チェック
            'category_id' => [
                'required',
                Rule::exists('categories', 'id')->where(function ($query) {
                    $query->where('is_active', true);
                })
            ],
            
            // 複合ユニーク制約
            'slug' => [
                'required',
                'string',
                'max:255',
                Rule::unique('posts')->where(function ($query) use ($request) {
                    return $query->where('user_id', auth()->id())
                                ->where('category_id', $request->category_id);
                })
            ],
            
            // リレーション存在チェック
            'tags' => 'array',
            'tags.*' => [
                Rule::exists('tags', 'id')->where(function ($query) {
                    $query->where('is_published', true);
                })
            ]
        ]);

        // 処理を継続...
    }
}

// バリデーションサービスクラス
namespace App\Services;

use Illuminate\Support\Facades\Validator;

class ValidationService
{
    /**
     * 日本の住所バリデーション
     */
    public static function validateJapaneseAddress(array $data): array
    {
        $rules = [
            'postal_code' => [
                'required',
                'regex:/^\d{3}-?\d{4}$/'
            ],
            'prefecture' => [
                'required',
                Rule::in(config('constants.prefectures'))
            ],
            'city' => [
                'required',
                'string',
                'max:50'
            ],
            'address' => [
                'required',
                'string',
                'max:100'
            ],
            'building' => [
                'nullable',
                'string',
                'max:100'
            ]
        ];

        $messages = [
            'postal_code.required' => '郵便番号を入力してください',
            'postal_code.regex' => '郵便番号は123-4567の形式で入力してください',
            'prefecture.required' => '都道府県を選択してください',
            'prefecture.in' => '有効な都道府県を選択してください',
            'city.required' => '市区町村を入力してください',
            'address.required' => '番地を入力してください'
        ];

        $validator = Validator::make($data, $rules, $messages);

        // 郵便番号API連携(オプション)
        $validator->after(function ($validator) use ($data) {
            if (!$validator->errors()->has('postal_code')) {
                $postalCode = str_replace('-', '', $data['postal_code']);
                // 郵便番号APIを使用した住所検証
                // $addressData = PostalCodeAPI::lookup($postalCode);
                // if (!$addressData) {
                //     $validator->errors()->add('postal_code', '有効な郵便番号を入力してください');
                // }
            }
        });

        if ($validator->fails()) {
            throw new \Illuminate\Validation\ValidationException($validator);
        }

        return $validator->validated();
    }

    /**
     * クレジットカード情報のバリデーション
     */
    public static function validateCreditCard(array $data): array
    {
        $currentYear = date('Y');
        $currentMonth = date('n');

        $rules = [
            'card_number' => [
                'required',
                'string',
                'regex:/^\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}$/',
                function ($attribute, $value, $fail) {
                    // Luhnアルゴリズムでカード番号を検証
                    $number = preg_replace('/\D/', '', $value);
                    if (!self::luhnCheck($number)) {
                        $fail('有効なカード番号を入力してください');
                    }
                }
            ],
            'card_holder' => [
                'required',
                'string',
                'max:100',
                'regex:/^[A-Z\s]+$/i'
            ],
            'expiry_month' => [
                'required',
                'integer',
                'between:1,12'
            ],
            'expiry_year' => [
                'required',
                'integer',
                "min:{$currentYear}",
                'max:' . ($currentYear + 10)
            ],
            'cvv' => [
                'required',
                'digits_between:3,4'
            ]
        ];

        $validator = Validator::make($data, $rules);

        // 有効期限チェック
        $validator->after(function ($validator) use ($data, $currentYear, $currentMonth) {
            if (!$validator->errors()->has(['expiry_month', 'expiry_year'])) {
                if ($data['expiry_year'] == $currentYear && $data['expiry_month'] < $currentMonth) {
                    $validator->errors()->add('expiry_month', 'カードの有効期限が切れています');
                }
            }
        });

        if ($validator->fails()) {
            throw new \Illuminate\Validation\ValidationException($validator);
        }

        return $validator->validated();
    }

    /**
     * Luhnアルゴリズムによるカード番号検証
     */
    private static function luhnCheck(string $number): bool
    {
        $sum = 0;
        $length = strlen($number);
        $parity = $length % 2;

        for ($i = 0; $i < $length; $i++) {
            $digit = (int)$number[$i];
            if ($i % 2 == $parity) {
                $digit *= 2;
                if ($digit > 9) {
                    $digit -= 9;
                }
            }
            $sum += $digit;
        }

        return $sum % 10 == 0;
    }
}

APIレスポンス用バリデーション

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\JsonResponse;

class ApiValidationController extends Controller
{
    /**
     * API用バリデーション(JSON応答)
     */
    public function store(Request $request): JsonResponse
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8|confirmed',
            'device_token' => 'required|string',
            'platform' => 'required|in:ios,android,web'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'message' => 'バリデーションエラー',
                'errors' => $validator->errors(),
                'error_count' => $validator->errors()->count()
            ], 422);
        }

        // ユーザー作成処理
        $user = User::create($validator->validated());

        return response()->json([
            'success' => true,
            'message' => 'ユーザーが正常に作成されました',
            'data' => [
                'user' => $user,
                'token' => $user->createToken('api-token')->plainTextToken
            ]
        ], 201);
    }

    /**
     * バッチバリデーション(複数レコード)
     */
    public function batchCreate(Request $request): JsonResponse
    {
        $validator = Validator::make($request->all(), [
            'users' => 'required|array|min:1|max:100',
            'users.*.name' => 'required|string|max:255',
            'users.*.email' => 'required|email|distinct',
            'users.*.role' => 'required|in:admin,user,guest'
        ]);

        // カスタムバリデーション(メールの一意性チェック)
        $validator->after(function ($validator) use ($request) {
            if (!$validator->errors()->has('users.*.email')) {
                $emails = collect($request->users)->pluck('email');
                $existingEmails = User::whereIn('email', $emails)->pluck('email');
                
                foreach ($request->users as $index => $user) {
                    if ($existingEmails->contains($user['email'])) {
                        $validator->errors()->add(
                            "users.{$index}.email",
                            "メールアドレス {$user['email']} は既に使用されています"
                        );
                    }
                }
            }
        });

        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'message' => 'バリデーションエラー',
                'errors' => $validator->errors(),
                'failed_count' => $validator->errors()->count()
            ], 422);
        }

        // バッチ作成処理
        $createdUsers = [];
        foreach ($validator->validated()['users'] as $userData) {
            $createdUsers[] = User::create($userData);
        }

        return response()->json([
            'success' => true,
            'message' => count($createdUsers) . '件のユーザーが作成されました',
            'data' => [
                'users' => $createdUsers,
                'created_count' => count($createdUsers)
            ]
        ], 201);
    }
}