Form Request Validation

Validating requests using Form Request classes

graph TD A[HTTP Request] --> B[Form Request Class] B --> C{Authorize?} C -->|No| D[403 Forbidden] C -->|Yes| E[Prepare Data] E --> F[Validate Rules] F --> G{Valid?} G -->|No| H[Redirect with Errors] G -->|Yes| I[Controller Method] I --> J[Validated Data] K[Custom Messages] --> F L[Custom Rules] --> F

Form Request Validation is a method for validating requests using dedicated classes. This pattern allows you to separate validation logic from controller and have cleaner and more maintainable code.


Benefits of Form Request:


  • Separation of validation logic from business logic
  • Cleaner and more organized code
  • Reusability across multiple controllers
  • Authorization in the same class
  • Custom error messages
  • Custom validation rules
  • Automatic redirect on validation failure
  • Type-safe validated data

Features:


  • <strong>rules()</strong>: Define validation rules
  • <strong>messages()</strong>: Define custom error messages
  • <strong>attributes()</strong>: Define custom attribute names
  • <strong>authorize()</strong>: Check authorization
  • <strong>prepareForValidation()</strong>: Prepare data before validation
  • <strong>withValidator()</strong>: Access validator instance
  • <strong>after()</strong>: Execute logic after validation

Usage:


  • Create FormRequest class
  • Define rules and messages
  • Use in controller with type-hinting
  • Automatic validation and redirect

Examples

Creating Form Request

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
    public function authorize()
    {
        return true; // Or check permissions
    }
    
    public function rules()
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users,email',
            'password' => 'required|string|min:8|confirmed',
        ];
    }
    
    public function messages()
    {
        return [
            'email.unique' => 'This email is already taken.',
            'password.min' => 'Password must be at least 8 characters.',
        ];
    }
}

A Form Request that defines validation rules.

Usage in Controller

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreUserRequest;

class UserController extends Controller
{
    public function store(StoreUserRequest $request)
    {
        // If we reach here, validation passed
        $validated = $request->validated();
        
        User::create($validated);
        
        return redirect()->route('users.index')
            ->with('success', 'User created successfully');
    }
}

Using Form Request in Controller. If validation fails, it automatically redirects.

Authorization in Form Request

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateUserRequest extends FormRequest
{
    public function authorize()
    {
        // Check if user can update this user
        $user = $this->route('user');
        
        return $this->user()->can('update', $user);
        // Or
        // return $this->user()->id === $user->id;
    }
    
    public function rules()
    {
        return [
            'name' => 'sometimes|required|string|max:255',
            'email' => 'sometimes|required|email|unique:users,email,' . $this->route('user')->id,
        ];
    }
}

Authorization check in Form Request that runs before validation.

Dynamic Rules

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    public function rules()
    {
        $rules = [
            'title' => 'required|string|max:255',
            'content' => 'required|string',
        ];
        
        // Add image rule only if image is present
        if ($this->hasFile('image')) {
            $rules['image'] = 'required|image|max:2048';
        }
        
        // Different rules for different user roles
        if ($this->user()->isAdmin()) {
            $rules['published'] = 'sometimes|boolean';
        }
        
        return $rules;
    }
}

Dynamic rules that change based on conditions.

prepareForValidation

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreOrderRequest extends FormRequest
{
    protected function prepareForValidation()
    {
        // Merge additional data before validation
        $this->merge([
            'user_id' => auth()->id(),
            'total' => $this->calculateTotal(),
        ]);
        
        // Or transform existing data
        $this->merge([
            'email' => strtolower($this->email),
        ]);
    }
    
    public function rules()
    {
        return [
            'user_id' => 'required|exists:users,id',
            'items' => 'required|array',
            'total' => 'required|numeric|min:0',
        ];
    }
}

Preparing data before validation.

withValidator

<?php

namespace App\Http\Requests;

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

class StoreProductRequest extends FormRequest
{
    public function rules()
    {
        return [
            'name' => 'required|string',
            'price' => 'required|numeric|min:0',
            'category_id' => 'required|exists:categories,id',
        ];
    }
    
    public function withValidator(Validator $validator)
    {
        $validator->after(function ($validator) {
            // Custom validation logic
            if ($this->price < 0) {
                $validator->errors()->add('price', 'Price cannot be negative.');
            }
            
            // Check business rules
            $category = Category::find($this->category_id);
            if ($category && !$category->isActive()) {
                $validator->errors()->add('category_id', 'Selected category is not active.');
            }
        });
    }
}

Accessing validator instance for custom validation.

Custom Attributes

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
    public function rules()
    {
        return [
            'first_name' => 'required|string',
            'last_name' => 'required|string',
            'email_address' => 'required|email',
        ];
    }
    
    public function attributes()
    {
        return [
            'first_name' => 'first name',
            'last_name' => 'last name',
            'email_address' => 'email address',
        ];
    }
    
    // Error: "The first name field is required."
    // Instead of: "The first_name field is required."

Defining custom attribute names for error messages.

API Form Request

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

class ApiStoreUserRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }
    
    public function rules()
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
        ];
    }
    
    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(
            response()->json([
                'message' => 'Validation failed',
                'errors' => $validator->errors()
            ], 422)
        );
    }
}

Form Request for API that returns JSON response instead of redirect.

Use Cases

  • Separating validation logic from controllers
  • Reusable validation across multiple controllers
  • Authorization check before validation
  • Custom validation rules and messages
  • Data transformation before validation
  • Complex validation logic with business rules

Common Mistakes

  • Doing business logic in Form Request that should be in controller
  • Not using authorize() causing security issues
  • Forgetting return in authorize() causing errors
  • Using Form Request for simple validation which is over-engineering
  • Not handling API responses causing redirects
  • Nested validation causing complexity

Best Practices

  • Use Form Request for complex validation
  • Always implement authorize()
  • Use prepareForValidation() for data transformation
  • Define custom messages and attributes
  • Use dynamic rules for conditional validation
  • Separate API Form Request from Web Form Request
  • Test Form Request

Edge Cases

  • Form Request with file uploads
  • Nested array validation
  • Conditional validation based on user role
  • Form Request in API requiring JSON response
  • Validation with database constraints
  • Custom validation with business rules

Performance Notes

  • Form Request overhead is low
  • Validation caching can improve performance
  • Use eager loading in validation rules
  • Complex validation rules can be slow

Security Notes

  • Always implement authorize()
  • Use validation rules for input sanitization
  • Ensure sensitive data is properly validated
  • Use unique validation to prevent duplicate entries
  • File upload validation is important for security

Interview Points

  • What is the difference between Form Request and manual validation?
  • When does authorize() execute?
  • How can you define dynamic rules?
  • What is the difference between Web and API Form Request?
  • When is prepareForValidation() used?
  • How can you test Form Request?

Version Notes

  • Laravel 11.x: Improved performance in Form Request validation
  • Laravel 11.x: Better support for nested validation
  • Laravel 10.x: Improved error message formatting
  • Laravel 9.x: Added prepareForValidation() hook