Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Validation Laravel Package

illuminate/validation

The Illuminate Validation package provides a robust, framework-agnostic validation library for PHP. It offers dozens of built-in rules, custom rule support, nested array validation, and localized error messages, powering form and request validation in Laravel and standalone projects.

View on GitHub
Deep Wiki
Context7

Getting Started

In a Laravel app you already have it — composer require illuminate/validation is only needed if you're assembling the component standalone (and then you must also wire up a Translator/TranslationLoader, a Container, and Illuminate\Support\MessageBag, which is the main reason the docs tell you not to). Check composer.lock before adding anything; a version mismatch with laravel/framework is the usual cause of "class not found" surprises.

Start at three files, in this order:

  1. vendor/laravel/framework/src/Illuminate/Validation/Validator.php — the rule dispatch loop and validateAttribute(); this is where "why didn't my rule fire?" gets answered.
  2. vendor/laravel/framework/src/Illuminate/Validation/Concerns/ValidatesAttributes.php — every built-in rule signature, so you can see exactly what max:255 means for strings vs. arrays vs. files.
  3. resources/lang/en/validation.php (or lang/en/validation.php in Laravel 9+) — the message keys you'll be overriding.

First real use case: a form request on a write endpoint.

php artisan make:request StoreInvoiceRequest
public function authorize(): bool { return $this->user()->can('create', Invoice::class); }

public function rules(): array
{
    return [
        'customer_id' => ['required', 'integer', Rule::exists('customers', 'id')],
        'lines'       => ['required', 'array', 'min:1'],
        'lines.*.sku' => ['required', 'string', 'max:64'],
        'lines.*.qty' => ['required', 'integer', 'min:1'],
        'due_date'    => ['nullable', 'date', 'after_or_equal:today'],
    ];
}

Type-hint it in the controller and validation, authorization, and the 422/redirect are all handled for you. That single pattern — request class in, validated array out via $request->validated() — covers the majority of day-to-day usage; build everything else on top of it.

Implementation Patterns

Form requests as the default, inline validation as the exception. Use $request->validate([...]) for one-off endpoints (webhooks, health checks, tiny controllers) and promote to a form request the moment rules are reused, exceed ~8 rules, or need after() hooks. Move authorize() logic into policies early — a form request that returns true unconditionally is a security bug waiting for a copy-paste.

Never read from $request->all() after validating. Use validated() / safe(). This is the single highest-leverage habit for mass-assignment safety: it makes the rule set the contract, so a new column added to the table doesn't silently become fillable.

Nested and array rules with wildcards. lines.*.sku is the workhorse for line items, bulk imports, and JSON:API payloads. Combine with distinct on the parent key ('lines.*.sku' => ['distinct']) and array:sku,qty (Laravel 9+) to reject unknown keys instead of ignoring them.

Conditional rules: prefer Rule:: builders over required_if strings. Rule::requiredIf(fn () => $this->input('type') === 'business'), Rule::when($condition, [...], [...]), Rule::forEach(), and Rule::prohibitedIf() read better, are statically analyzable, and don't break when a value contains a pipe or comma.

after() hooks for cross-field and cross-record invariants. Anything needing the DB, the authenticated user, or two fields compared belongs here rather than in a custom rule.

public function after(): array
{
    return [function (Validator $v) {
        if ($this->filled('discount') && $this->input('discount') > $this->subtotal()) {
            $v->errors()->add('discount', __('Discount exceeds subtotal.'));
        }
    }];
}

Custom rules for reusable domain constraints. php artisan make:rule VatNumber gives you a ValidationRule object with validate($attribute, $value, $fail) — it's injectable from the container, so you can type-hint a repository or HTTP client in the constructor. Use DataAwareRule / ValidatorAwareRule when the rule needs sibling fields. Keep them pure: no side effects on failure.

prepareForValidation() for normalization. Trim strings, lowercase emails, coerce "1"/"on" checkboxes to booleans, and convert empty strings to null before rules run. Doing this in the form request beats doing it in the controller afterwards, because nullable then behaves the way you expect. Global TrimStrings/ConvertEmptyStringsToNull middleware covers the common cases only on web/API requests that pass through the HTTP kernel — queue jobs and console commands bypass it entirely.

Shared rule sets across HTTP and jobs/imports. Extract InvoiceRules::rules(): array into a plain class and call it from both the form request and an ImportRowValidator. This is where teams usually duplicate logic and drift; a static rules provider keeps console imports and API endpoints honest.

Testing. $this->postJson(...)->assertJsonValidationErrors(['lines.0.qty']) asserts both the failure and the key format. For unit-testing a rule class in isolation, use Validator::make(['vat' => 'X'], ['vat' => [new VatNumber]]) and assert ->fails() / ->errors()->first(). Seed with Http::fake() when the rule hits an external service.

Custom messages locally, translations globally. Per-request messages() and attributes() for field-name polish ('lines.*.qty' => 'quantity'), and lang/{locale}/validation.php for the messages themselves. Use :attribute, :min, :other placeholders as documented; run php artisan lang:publish on Laravel 9+ to get an editable copy.

Gotchas and Tips

  • Pipe strings are a parser, not syntax. 'email' => 'required|email|unique:users,email,NULL,id,deleted_at,NULL' works, but any rule whose parameters contain | or , (regex, in: with commas, Rule objects) breaks it. Rule of thumb: the moment a rule takes parameters, switch that field to array syntax. Mixing 'required|string' and [new Uppercase] in one array is fine; a Rule object inside a pipe string is not.

  • Empty means "skip" unless you say otherwise. Most rules don't run on null, '', or missing keys. nullable documents intent, sometimes makes it explicit that the field may be absent from the payload, and required overrides the skip. The classic bug is 'price' => ['numeric'] passing when price is missing entirely — add present or required when absence is meaningful. Note sometimes is applied per-field and interacts with wildcards in ways that surprise people; test lines.*.qty with an empty array.

  • Type rules are mutually exclusive and order-agnostic. integer and string on the same field will always fail one of them; numeric accepts numeric strings, so 'string','numeric' is legal but 'integer','numeric' is redundant. gt:5 compares numerically when both sides are numeric, lexically otherwise, and gt:field compares against another field name, not a literal — gt:5 vs gt:field fails silently if you forget the distinction. size means characters for strings, value for numbers, and kilobytes for files.

  • unique's ignore/soft-delete signature is positional and order-dependent. unique:table,column,exceptId,idColumn,whereColumn,whereValue — the fourth parameter defaults to id and the fifth/sixth form an extra where clause. Use Rule::unique('users','email')->ignore($user)->whereNull('deleted_at') instead of counting commas; it's the difference between a working rule and a rule that silently ignores the wrong row. Wrap multi-column uniqueness in ->where(fn ($q) => ...).

  • Hidden and unchecked inputs never arrive. boolean fails on an absent checkbox; accepted needs the field present. Use $request->boolean('terms') after prepareForValidation(), or add 'terms' => ['sometimes', 'accepted'] plus a default merge. Similarly, nullable on a file field does not discard an empty file upload from some browsers — check with hasFile() before trusting the rule.

  • ValidationException is control flow. It's caught by the exception handler and turned into a redirect-with-errors for web or a 422 JSON body for expectsJson(). If you call Validator::make() directly, you own that: ->validate() throws, ->fails() doesn't. In API resources and jobs, exceptions thrown inside after() or custom rules are not converted to validation errors — they bubble as 500s. Use $fail() there, never throw.

  • Message keys are literal rule names; attribute names are literal payload keys. 'required_if', not 'required if'; 'lines.0.qty' falls back to 'lines.*.qty' in messages(), but attributes() needs the wildcard form spelled the same way. Dotted field names get exploded in the fallback chain, so a field literally named user.name will not resolve the message you think it will. Alias it in attributes().

  • Debugging. $validator->errors()->toArray() for the message map, $validator->failed() for [field => [Rule => params]] — the latter is the fastest way to see which rule actually tripped when several could have. Validator::make(...) with $stopOnFirstFailure = true short-circuits and hides downstream failures; keep it off while debugging. For "my rule never runs," check the input is present and non-empty first — nine times out of ten it's the empty-skip rule above, not the rule itself.

  • Extension points and their cost. Validator::extend() / extendImplicit() in a service provider is the quick path for legacy regex, but global string rules are invisible in the codebase and collide with future Laravel rule names. Prefer rule objects; reserve extend() for third-party compatibility. Validator::replacer() changes how :attribute is rendered globally — scope that carefully, since it affects every message in the app. Custom rule names registered this way also need a message entry, or users see the raw key.

  • Standalone use is where it bites. Without a Laravel Translator, messages come back as raw keys like validation.required; without the container, Rule::exists and database-backed rules fail. If you're pulling this package into a Slim or plain-PHP app, budget for a translator and a Capsule/DB connection — or take a hard look at symfony/validator, which is designed to be framework-free.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mydnic/volet-feature-board
wmde/fundraising-phpcs
friendlylabs/surveyr
mg-techlegend/laravel-notify-africa
islamabdurahman/laravel-sms-service
dmhendricks/b2-sdk-php
aplicativa/laravel-admin
ezavalishin/vkma
jackardios/laravel-json-api-paginate
viral/laravel-adminpanel
dieppk/log-viewer
daimakuai-ext/helpers
codexshaper/php-blade-view
abraxas81/charts
adamlien/laravel-cas
quimgc/tasks
sistem-atc/bank-sdk
core45/laravel-cookieconsent
projectsend/community-modules
ovac/laravel-hubtel-payment