paulund

Form Request Validation Patterns

Form Request Validation Patterns

Form Request classes are Laravel's dedicated mechanism for validation and authorisation. Moving validation out of your controllers keeps them slim and makes your validation rules reusable and testable.

The Basics

Generate a Form Request with Artisan and fill in the rules() and authorize() methods:

php artisan make:request StoreArticleRequest
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreArticleRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', \App\Models\Article::class);
    }

    public function rules(): array
    {
        return [
            'title'   => ['required', 'string', 'max:255'],
            'body'    => ['required', 'string'],
            'status'  => ['required', 'in:draft,published'],
            'tags'    => ['sometimes', 'array', 'max:10'],
            'tags.*'  => ['string', 'max:50'],
        ];
    }
}

Type-hint it in your controller and Laravel handles validation and authorisation before your method body runs:

class ArticleController extends Controller
{
    public function store(StoreArticleRequest $request): RedirectResponse
    {
        $article = Article::create($request->validated());

        return redirect()->route('articles.show', $article);
    }
}

Custom Validation Messages

Override the default messages for any rule to give your users clearer feedback:

public function messages(): array
{
    return [
        'title.required' => 'Every article needs a title.',
        'status.in'      => 'Status must be either "draft" or "published".',
        'tags.max'       => 'You can add up to 10 tags per article.',
    ];
}

Custom Validation Rules

For logic that does not fit a built-in rule, create a reusable rule class:

php artisan make:rule UniqueSlug
namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use App\Models\Article;

class UniqueSlug implements Rule
{
    public function __construct(
        private ?int $excludeId = null,
    ) {}

    public function passes(string $attribute, mixed $value): bool
    {
        $query = Article::where('slug', $value);

        if ($this->excludeId !== null) {
            $query->whereNot('id', $this->excludeId);
        }

        return !$query->exists();
    }

    public function message(): string
    {
        return 'This slug is already taken by another article.';
    }
}

Use it in your Form Request:

public function rules(): array
{
    return [
        'slug' => ['required', 'string', new UniqueSlug($this->article?->id)],
    ];
}

Conditional Validation

Sometimes a field is only required depending on another field's value. Use sometimes and required_if:

public function rules(): array
{
    return [
        'payment_method'      => ['required', 'in:card,bank_transfer,invoice'],
        'card_number'         => ['required_if:payment_method,card', 'sometimes', 'string'],
        'bank_account_number' => ['required_if:payment_method,bank_transfer', 'sometimes', 'string'],
        'company_name'        => ['required_if:payment_method,invoice', 'sometimes', 'string'],
    ];
}

For more complex logic, define rules dynamically inside rules():

public function rules(): array
{
    $rules = [
        'email'    => ['required', 'email'],
        'password' => ['required', 'min:8', 'confirmed'],
    ];

    if ($this->input('role') === 'admin') {
        $rules['mfa_secret'] = ['required', 'string'];
    }

    return $rules;
}

Sharing Validation Logic

If multiple Form Requests share the same set of rules — for example, both StoreArticleRequest and UpdateArticleRequest validate the same fields — extract shared rules into a base class:

abstract class ArticleRequest extends FormRequest
{
    protected function articleRules(): array
    {
        return [
            'title'  => ['required', 'string', 'max:255'],
            'body'   => ['required', 'string'],
            'status' => ['required', 'in:draft,published'],
        ];
    }
}

class StoreArticleRequest extends ArticleRequest
{
    public function rules(): array
    {
        return $this->articleRules();
    }
}

class UpdateArticleRequest extends ArticleRequest
{
    public function rules(): array
    {
        return array_merge($this->articleRules(), [
            'slug' => ['required', 'string', new UniqueSlug($this->article->id)],
        ]);
    }
}

Tips

  • Always use array syntax for rules (['required', 'email']) rather than pipe-separated strings ('required|email'). Array syntax is clearer and avoids ambiguity when a rule argument contains a pipe character.
  • Call $request->validated() in your controller, not $request->all(). It returns only the fields that passed validation, protecting you from mass assignment.
  • The authorize() method runs before validation. If it returns false, Laravel returns a 403 without ever touching the rules.