Form Request Validation
Validating requests using Form Request classes
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