Introduction
Form Request classes move validation logic out of controllers into reusable, testable classes. This lesson covers authorize(), rules(), custom messages, prepareForValidation(), and the sometimes() helper.
Key Concepts
FormRequestclass: A custom request type underapp/Http/Requestswith its own rules and authorization.authorize(): Returns a boolean that Laravel uses to respond with 403 before validation runs.rules(): Returns an array of validation rules.prepareForValidation(): Lets you mutate input before validation (e.g. trim, slugify).validated()/safe(): Returns the validated subset of input.
Real World Context
Validation in controllers quickly bloats into hundreds of lines per action. Form Requests centralise the rules, make them unit-testable, and keep controllers focused on coordination.
Deep Dive
Form requests are custom request classes that contain validation logic. They keep your controllers clean and make validation reusable.
Creating Form Requests
bashphp artisan make:request StorePostRequest
This creates app/Http/Requests/StorePostRequest.php:
php<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StorePostRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; // or add authorization logic } /** * Get the validation rules that apply to the request. */ public function rules(): array { return [ 'title' => 'required|string|max:255', 'body' => 'required|string', 'category_id' => 'required|exists:categories,id', ]; } }
Using Form Requests in Controllers
Type-hint the form request instead of Request:
phpuse App\Http\Requests\StorePostRequest; class PostController extends Controller { public function store(StorePostRequest $request) { // Validation happens automatically! // If validation fails, user is redirected with errors // Access validated data $validated = $request->validated(); $post = Post::create($validated); return redirect()->route('posts.show', $post); } }
Authorization in Form Requests
phppublic function authorize(): bool { // Check if user owns the resource $post = $this->route('post'); // Get route model binding return $post && $this->user()->id === $post->user_id; } // Or use policies public function authorize(): bool { return $this->user()->can('create', Post::class); }
If authorization fails, a 403 response is returned.
Custom Error Messages
phppublic function messages(): array { return [ 'title.required' => 'Please enter a post title.', 'title.max' => 'The title cannot exceed 255 characters.', 'body.required' => 'The post body is required.', 'category_id.exists' => 'Please select a valid category.', ]; }
Custom Attribute Names
phppublic function attributes(): array { return [ 'category_id' => 'category', 'user_id' => 'author', ]; } // Instead of "The category_id field is required." // Shows: "The category field is required."
Preparing Input for Validation
Modify input before validation:
phpprotected function prepareForValidation(): void { $this->merge([ 'slug' => Str::slug($this->title), 'user_id' => $this->user()->id, ]); }
Accessing Validated Data
phppublic function store(StorePostRequest $request) { // All validated data $validated = $request->validated(); // Only specific keys $validated = $request->safe()->only(['title', 'body']); // All except specific keys $validated = $request->safe()->except(['category_id']); // Merge additional data $validated = $request->safe()->merge(['user_id' => auth()->id()]); }
Conditional Validation Rules
phppublic function rules(): array { return [ 'email' => 'required|email', 'role' => 'required|string', // Only validate password for new users 'password' => $this->isMethod('post') ? 'required|string|min:8' : 'nullable|string|min:8', ]; } // Or use the sometimes method protected function withValidator($validator) { $validator->sometimes('reason', 'required', function ($input) { return $input->role === 'admin'; }); }
After Validation Hook
phpprotected function passedValidation(): void { // Called after validation passes // Good for cleaning up or transforming data $this->replace([ 'title' => strip_tags($this->title), ]); }
Practical Example: UpdatePostRequest
php<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; class UpdatePostRequest extends FormRequest { public function authorize(): bool { $post = $this->route('post'); return $this->user()->can('update', $post); } public function rules(): array { return [ 'title' => [ 'required', 'string', 'max:255', // Unique except for this post Rule::unique('posts')->ignore($this->route('post')), ], 'body' => 'required|string|min:100', 'category_id' => 'required|exists:categories,id', 'tags' => 'nullable|array', 'tags.*' => 'exists:tags,id', 'published_at' => 'nullable|date|after:now', ]; } public function messages(): array { return [ 'body.min' => 'Posts must be at least 100 characters long.', 'tags.*.exists' => 'One or more selected tags are invalid.', ]; } }
Common Pitfalls
- Returning
truefromauthorize()everywhere — Free-for-all access defeats the purpose. Call your policies or check ownership. - Mixing unvalidated input into the model — Always call
$request->validated()rather than$request->all().
Best Practices
- One Form Request per controller action —
StorePostRequestandUpdatePostRequestkeep rules focused. - Delegate authorization to policies —
$this->user()->can('create', Post::class)keeps authorization consistent across the app.
Summary
- Form Requests move validation out of the controller.
authorize()returns a boolean and can trigger 403.rules()defines the validation rules array.prepareForValidation()mutates input before rules run.- Always use
validated()to fetch clean input.