Topic 16 of 75·M15
Validation
Must know`$request->validate()` · Form Requests · custom rules · conditional · nested · custom messages · `authorize()` + `rules()`
The one-sentence answer
Validation is the gatekeeper between untrusted input and your application — every request is validated at the boundary, before a line of business logic runs.
The mental model
Input arrives untrusted — from forms, APIs, JSON, anything. Validation is the filter at the door:
raw request
│
▼
VALIDATE ── fail ──► 422 + field errors (Laravel does this for you)
│
▼ pass
validated data ──► controller / service (only safe values from here on)
Laravel's shape is $request->validate([rules]) or a Form Request class. Both produce a 422 with structured field errors on failure — the frontend renders them, no controller code runs.
How it works
The inline form
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'items' => ['required', 'array', 'min:1'],
'items.*.id' => ['required', 'exists:products,id'], // nested
]);
// $validated is safe — use it, never $request->all()
}
Form Requests — the senior shape
class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Order::class);
}
public function rules(): array
{
return [
'items' => ['required', 'array', 'min:1'],
'items.*.quantity' => ['required', 'integer', 'min:1', 'max:99'],
];
}
public function messages(): array
{
return ['items.*.quantity.max' => 'No more than 99 per item.'];
}
}
The controller takes the request, and the request is already validated and authorized:
public function store(StoreOrderRequest $request)
{
Order::create($request->validated());
}
authorize() gates who may submit; rules() gates what's valid. Both run before the controller.
Conditional validation
'email' => ['required_if:account_type,pro', 'email'],
// or with Rule::when / sometimes:
$request->validate([
'coupon' => ['sometimes', 'string'],
]);
sometimes skips validation when the field is absent; required_if makes a field required only under a condition.
Custom rules
// inline closure
'username' => [Rule::unique('users')->ignore($user->id), fn ($a, $v, $fail) => ...],
// a dedicated rule class
php artisan make:rule ValidCoupon
// implements passes() and message(), can inject services
Authorization inside Form Requests
The authorize() method is the same as a policy check — if it returns false, the request is rejected with 403 before validation even matters. Form requests are the clean home for both.
The plain-JS model (what the exercise does)
function validate(data, rules) {
const errors = {};
for (const [field, checks] of Object.entries(rules)) {
for (const check of checks) {
const err = check(data[field]);
if (err) { errors[field] = err; break; } // first failure per field
}
}
return Object.keys(errors).length ? { errors } : { valid: true };
}Interview questions
Q1. What happens when validation fails?
Laravel throws a
ValidationExceptionwith the field errors. For a web request, it redirects back with the errors flashed (and old input); for an API request, it returns 422 Unprocessable Entity with a JSON errors object. The controller never runs — the request is stopped at the gate.
Q2. Form Request vs $request->validate()?
$request->validate()is the quick inline form. A Form Request is a class withrules(),authorize(),messages(), and custom logic — the right choice when validation is reused, is more than a few rules, or carries authorization. The controller signaturestore(StoreOrderRequest $request)makes the boundary explicit.
Q3. What does authorize() do in a Form Request?
It's the authorization gate for the request — typically
$this->user()->can('create', Order::class). Returning false yields a 403 before validation. It keeps authorization next to the validation it protects, so the controller stays thin.
Q4. How do you validate nested data?
With dot notation:
'items.*.id' => 'exists:products,id','items.*.quantity' => 'integer|min:1'. The*wildcard applies the rule to every element. Errors come back keyeditems.2.quantity, which frontends render against the matching field.
Q5. What is conditional validation?
Rules that only apply under conditions —
required_if:account_type,pro,sometimes(only if present), orRule::when($condition, [...]). Use them so a single rule set serves multiple paths instead of duplicating validation.
Senior follow-up: Where should validation live in a service-layer architecture?
At the boundary — the Form Request (or an API validation in the controller). Services assume their inputs are already valid; that keeps services callable from commands, jobs, and tests without re-validating, and keeps the validation contract visible at the HTTP edge. The rule: validate at the door, trust the data inside.
Common mistakes
❌ Using $request->all() in the controller — validated data comes from $request->validated().
❌ Validating inside services — the service should assume valid input (validated at the boundary).
❌ Business rules as ad-hoc if checks scattered in controllers — put them in rules or a Form Request.
❌ Raw-outputting validation messages without escaping — the errors are user-influenced (Lesson 37).
Quick revision notes
- Validate at the boundary; the controller only ever sees
validated() $request->validate()= inline · Form Request = reusable +authorize()+messages()- Failure = 422 + field errors (API) / redirect + errors (web)
- Nested:
items.*.id· Conditional:sometimes,required_if,Rule::when authorize()gates 403 before validation — request-level authz
Check your understanding
Answer each one out loud. Then rate yourself honestly.
0/5What does Laravel return on validation failure for web vs API?
When do you reach for a Form Request over inline validation?
How does authorize() interact with rules()?
Write the nested rule for an array of items with quantity.
Why should services assume validated input?