Form Requests & Advanced Validation
Move validation out of controllers into dedicated, reusable, authorizable request classes.
Inline $request->validate([...]) is fine for a toy, but real endpoints accumulate rules, authorization, and data massaging. A Form Request is a dedicated class that owns all of it, leaving your controller method a clean two-liner.
Three methods do the work
authorize()— returntrue/false(or a Gate check). If it fails, a 403 before rules even run.rules()— the validation rules, where you can use array/object rule builders.messages()/attributes()— humane error text.
class StorePostRequest extends FormRequest
{
public function authorize(): bool { return $this->user()->can('create', Post::class); }
public function rules(): array {
return ['title' => ['required', 'string', 'max:255']];
}
}
// Controller: public function store(StorePostRequest $r) { Post::create($r->validated()); }Beyond simple rules
Use prepareForValidation() to normalise input before rules run, withValidator() for cross-field checks with after(), and rule objects like Rule::unique(...)->ignore($id) for context-aware constraints.
Example
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class UpdateProjectRequest extends FormRequest
{
public function authorize(): bool
{
// Route-model-bound {project}; policy decides.
return $this->user()->can('update', $this->route('project'));
}
protected function prepareForValidation(): void
{
// Normalise BEFORE rules run.
$this->merge(['slug' => \Illuminate\Support\Str::slug((string) $this->name)]);
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:120'],
'slug' => ['required', Rule::unique('projects')->ignore($this->route('project'))],
'budget'=> ['nullable', 'integer', 'min:0'],
'tags' => ['array', 'max:10'],
'tags.*'=> ['string', 'distinct'],
];
}
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $v) {
if ($this->budget && $this->budget > $this->user()->budget_cap) {
$v->errors()->add('budget', 'Exceeds your approval limit.');
}
});
}
}
// Controller stays tiny:
// public function update(UpdateProjectRequest $r, Project $project) {
// $project->update($r->validated());
// }When to use it
- A developer moves a 20-line validate() block out of a controller into a StoreProductRequest class so the controller method is reduced to a single validated line.
- An authorization check in authorize() rejects the request with a 403 if the authenticated user does not own the resource being updated, before rules() even runs.
- A form request's prepareForValidation() hook normalises the 'price' field from a comma-formatted string to a float before the numeric rule validates it.
More examples
Complete Form Request class
authorize() gates the request by policy; rules() declares typed validation constraints — all before the controller method runs.
class UpdateProductRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('update', $this->route('product'));
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:200'],
'price' => ['required', 'numeric', 'min:0'],
'stock' => ['required', 'integer', 'min:0'],
'tags' => ['array'],
'tags.*'=> ['string', 'exists:tags,slug'],
];
}
}Use the Form Request in a controller
Type-hinting the Form Request triggers automatic validation and authorization; validated() returns only the safe, declared fields.
public function update(
UpdateProductRequest $request,
Product $product
): RedirectResponse {
// $request->validated() contains only the declared rule keys
$product->update($request->validated());
return redirect()->route('products.show', $product)
->with('success', 'Product updated.');
}Normalise input with prepareForValidation()
prepareForValidation() fires before rules() and lets you sanitise or transform input data so the validation rules receive clean values.
class StoreOrderRequest extends FormRequest
{
protected function prepareForValidation(): void
{
// Convert '1,299.99' -> 1299.99 before validation
$this->merge([
'total' => (float) str_replace(',', '', $this->total),
'email' => strtolower(trim($this->email)),
]);
}
public function rules(): array
{
return [
'total' => ['required', 'numeric', 'min:0.01'],
'email' => ['required', 'email'],
];
}
}
Discussion