In a Laravel form request, in what order do prepareForValidation(), authorize(), rules(), after() and passedValidation() run, and what belongs in each?
answer
- prepare comes first, even before authorize
- merge() to normalise input
- after() callables run even when rules failed
- passedValidation() only on success
- validator keeps its own copy of data
basics
~20 sprepareForValidation() runs first, then authorize(), then the validator built from rules() plus after() callables, then passedValidation() only on success. Normalise input in the first, decide access in the second, cross-field checks in after(), and post-success tweaks last.
solid answer
~30 s`validateResolved()` calls `prepareForValidation()` first — the place to `merge()` normalised input, such as turning a checkbox into a real boolean. Then `authorize()`; a denial throws before any rule runs. Then the validator is created from `rules()`, `messages()` and `attributes()`, and the callables returned by `after()` are attached; they run after the rules, **even when rules already failed**, so they must guard against missing data. If the validator has errors, `ValidationException` is thrown. Otherwise `passedValidation()` runs — but the validator holds its own copy of the data, so changing input there does not change `validated()`.
code
php · 35 lines<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class StoreRegistrationRequest extends FormRequest
{
protected function prepareForValidation(): void
{
$this->merge(['has_dietary_needs' => $this->boolean('has_dietary_needs')]);
}
public function rules(): array
{
return [
'ticket_type' => ['required', 'in:standard,vip'],
'has_dietary_needs' => ['boolean'],
'dietary_notes' => ['required_if_accepted:has_dietary_needs', 'nullable', 'string'],
];
}
public function after(): array
{
return [function (Validator $validator) {
if ($validator->errors()->isNotEmpty()) {
return;
}
if ($this->route('event')->isSoldOut($this->input('ticket_type'))) {
$validator->errors()->add('ticket_type', 'This ticket type is sold out.');
}
}];
}
}go deeper
Know that prepareForValidation() lets you clean input before the rules run and that after() adds extra checks.
Recite the full order, including that preparation precedes authorization, and what each hook is meant for.
Guard after() callbacks against failed rules, keep authorization off merged input, and know passedValidation() does not change validated().
Decide which checks stay in the request layer and which move into domain services, so business invariants hold outside HTTP too.
## The sequence When the container resolves a form request, `validateResolved()` runs these steps in this order: 1. **`prepareForValidation()`** — empty by default; override it to adjust input. 2. **`authorize()`** — `false` (or a denying `Response`) throws `AuthorizationException`. 3. **Validator construction** — `rules()`, `messages()` and `attributes()` go into `Validator::make()`; then `withValidator($validator)` if you defined it; then the callables from **`after()`** are attached with `$validator->after(...)`. 4. **Validation** — the rules run, then every `after` callback runs. 5. **Failure** — if the error bag is not empty, `failedValidation()` throws `ValidationException`. 6. **`passedValidation()`** — empty by default; runs only when validation succeeded. Two details surprise people: preparation runs **before authorization**, and `after()` callbacks run **regardless of whether the rules passed**. ## prepareForValidation(): shape the input the rules will see The validator reads `validationData()`, which is `$this->all()`. Anything you `merge()` here becomes part of that data. Typical uses in an event-registration form: - Turn an HTML checkbox into a real boolean: `'has_dietary_needs' => $this->boolean('has_dietary_needs')`. - Trim or lowercase a promo code so a rule like `exists` compares like with like. - Build a derived field such as a slug. Keep it to **normalisation**. Because it runs before `authorize()`, values merged here are visible to the authorization check; never compute something that authorization trusts from raw input. And a merged key only reaches `validated()` if `rules()` has a rule for it. ## authorize(): access, not data Answer "may this user submit this?" — usually via a policy, the bound route model, or a registration-window flag. Nothing about field shapes belongs here. ## rules(): the declarative part Return the array of field rules. It is called through the container, so you may type-hint services, and because `prepareForValidation()` already ran, rules can depend on normalised values (for example, a conditional requirement on `dietary_notes` that reads the boolean you just produced). ## after(): checks a single rule cannot express `after()` returns an **array** of callables — closures or invokable objects — each receiving the `Validator`. Use it for checks that span fields or need a query: - The event still has seats for the requested ticket type. - The attendee is not already registered. - A combination of fields is inconsistent. Add an error with `$validator->errors()->add('ticket_type', 'Sold out.')`. Because these run after the rules even on failure, a callback should bail out when earlier rules failed, for example by checking `$validator->errors()->isNotEmpty()` or `$validator->failed()`, otherwise it may query with a missing or malformed value. | Hook | Runs when | Good for | Avoid | |---|---|---|---| | `prepareForValidation()` | always, first | normalising, casting, defaults | anything authorization relies on | | `authorize()` | always, second | access decisions | data-shape checks | | `rules()` | building the validator | field rules | side effects | | `after()` | after rules, pass or fail | cross-field or database checks | assuming the rules passed | | `passedValidation()` | only on success | post-processing request input | expecting `validated()` to change | ## passedValidation(): after success This hook runs once the data is known to be valid. The docs show `$this->replace([...])` there, and it does change what `$request->input()` returns afterwards. It does **not** change `$request->validated()`: the validator captured its own data array when it was built, and `validated()` reads from that copy. If the persisted data must include a post-processed value, add it explicitly with `safe()->merge([...])` in the controller, or override `validated()`. ## Choosing where logic goes - Normalisation → `prepareForValidation()`. - Per-field constraints → `rules()`. - Cross-field and database-backed constraints → `after()`. - Anything that must be written → derive it from `validated()` or merge it explicitly. A common review finding is a form request whose `after()` callback repeats checks that `rules()` already expresses, or a `prepareForValidation()` that quietly fills in defaults the business never agreed to. Keep each hook to its one job and the class stays readable: someone opening it should see, top to bottom, how input is cleaned, who may submit, what shape is required, and which cross-field facts are enforced.
- Why must an after() callback in a form request guard against earlier failures?`Validator::passes()` runs every rule, then every `after` callback unconditionally. If `ticket_type` was missing or invalid, a callback that queries by it gets a null or unexpected value. Checking `$validator->errors()->isNotEmpty()` first keeps the callback to data that already passed.
- What is the difference between after() and withValidator() on a form request?Both are optional. `withValidator($validator)` receives the built validator so you can configure it, for example by registering your own `after` callback. `after()` returns an array of callables or invokable objects that the form request attaches with `$validator->after()`. The docs lead with `after()`.
Think of an airline check-in: staff first tidy your paperwork (prepare), then confirm you hold a booking (authorize), then weigh each bag (rules), then check the total against your allowance (after), and only then print the boarding pass (passedValidation).
saying these in an interview costs you the question
- authorize() runs before prepareForValidation(), so merged values are not seen
- after() callbacks only run when every rule passed
- Changing input in passedValidation() updates validated()
- after() must return a single closure, not an array