0

Form Requests — authorize(), rules(), messages(), attributes()

Intermediate5 min read·lv-11-007

Concept

A FormRequest is a subclass of Illuminate\Foundation\Http\FormRequest (which extends Illuminate\Http\Request) that encapsulates both authorisation logic and validation rules for a specific HTTP action. You generate one with php artisan make:request StoreUserRequest.

The key methods are authorize(), rules(), messages(), and attributes(). When a FormRequest is type-hinted in a controller method signature, the service container resolves it and the ValidatesWhenResolvedTrait::validateResolved() method is called automatically during resolution. This trait calls authorize() first, then prepareForValidation() (a hook for normalising input before rules run), then passes rules() to a freshly created Validator instance.

authorize() must return a boolean. If it returns false, the framework throws Illuminate\Auth\Access\AuthorizationException, which the exception handler converts to a 403 Forbidden response. A common pattern is to use the Gate or policy directly inside this method: return Gate::allows('update', $this->route('post')). Returning true unconditionally is valid for public endpoints, but always be explicit — never leave a FormRequest with return false in authorize() (the generated stub default) in production.

rules() returns a key-value array exactly like what you pass to $request->validate(). The FormRequest itself is the request, so you can use $this->input('field'), $this->route('id'), and $this->user() inside rules() to make rules dynamic — for example, scoping a unique rule to ignore the current model during an update.

messages() returns an associative array overriding specific error messages: ['email.required' => 'We need your email address.']. The key format is field.rule. attributes() returns human-friendly field name overrides: ['email' => 'email address'], used in the default message strings like "The email address field is required."

The prepareForValidation() hook runs before rules and is the right place to transform input (cast strings to integers, trim whitespace, normalise phone number formats) without putting that logic in the controller.

Code Example

php
<?php

// app/Http/Requests/UpdatePostRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Models\Post;

class UpdatePostRequest extends FormRequest
{
    /**
     * Determine if the authenticated user is authorised to update this post.
     * $this->route('post') is the Post model resolved via route model binding.
     */
    public function authorize(): bool
    {
        $post = $this->route('post'); // instanceof Post thanks to route model binding
        return $this->user()->can('update', $post);
    }

    /**
     * Normalise input before validation runs.
     * Called by ValidatesWhenResolvedTrait before rules().
     */
    protected function prepareForValidation(): void
    {
        $this->merge([
            'slug' => \Illuminate\Support\Str::slug($this->input('title', '')),
        ]);
    }

    /**
     * The validation rules.
     * $this->route('post') is available here too.
     */
    public function rules(): array
    {
        $postId = $this->route('post')->id;

        return [
            'title'      => ['required', 'string', 'min:5', 'max:255'],
            'slug'       => ['required', 'string', Rule::unique('posts', 'slug')->ignore($postId)],
            'body'       => ['required', 'string', 'min:20'],
            'status'     => ['required', Rule::in(['draft', 'published', 'archived'])],
            'tags'       => ['nullable', 'array', 'max:10'],
            'tags.*'     => ['integer', Rule::exists('tags', 'id')],
            'published_at' => ['nullable', 'date', 'after_or_equal:today'],
        ];
    }

    /**
     * Custom error messages for specific rule failures.
     */
    public function messages(): array
    {
        return [
            'title.min'           => 'The post title must be at least 5 characters long.',
            'slug.unique'         => 'This URL slug is already taken — please edit the title.',
            'tags.*.exists'       => 'One or more selected tags do not exist.',
        ];
    }

    /**
     * Custom attribute names used in default message strings.
     */
    public function attributes(): array
    {
        return [
            'body'         => 'post body',
            'published_at' => 'publish date',
        ];
    }
}

// Controller — the FormRequest is resolved and validated before the method runs
class PostController extends \App\Http\Controllers\Controller
{
    public function update(UpdatePostRequest $request, Post $post): \Illuminate\Http\RedirectResponse
    {
        // Execution only reaches here if authorize() returned true and validation passed
        $post->update($request->validated());
        return redirect()->route('posts.show', $post);
    }
}

Interview Q&A

Q: At what point in the Laravel request lifecycle does FormRequest validation actually run?

When the FormRequest class is type-hinted in a controller method, the service container resolves it using Container::make(). During resolution, the container calls the FormRequest's validateResolved() method (from ValidatesWhenResolvedTrait). This happens inside the container's resolution pipeline, before the controller method body is entered. The sequence is: authorize() check, then prepareForValidation(), then passesAuthorization() check, then validator() is constructed from rules(), then validate() is called on it. If anything fails, an exception is thrown and the controller method body never executes.


Q: How can you use $this->route() inside FormRequest::rules(), and what are the caveats?

FormRequest extends Request, and route parameters are accessible via $this->route('parameter_name'). Because FormRequest is resolved after route model binding, if the parameter is type-hinted in the route definition, $this->route('post') returns the already-resolved Post model instance, not just the raw ID string. This lets you call $this->route('post')->id inside rules() to build a unique()->ignore() constraint. The caveat is that route binding can fail before FormRequest is resolved — if the model isn't found, a ModelNotFoundException (404) is thrown by the router before validation even begins.


Q: What is the difference between overriding messages() in a FormRequest versus using translation files?

messages() returns hard-coded strings embedded in the PHP class itself. This is convenient for project-specific overrides but does not support localisation. Translation files (lang/en/validation.php and per-file lang/en/messages.php) allow the same overrides with full multi-language support via __('validation.required') keys. The Validator checks for custom messages in this order: the messages() array from FormRequest first, then $customMessages passed to Validator::make(), and finally the translation file. For multi-locale applications, always use translation files; use messages() only for single-locale projects or one-off message overrides.