laravel-validation
SKILL.md
Laravel Validation
Always use array syntax for rules, not pipe-delimited strings. Arrays are easier to read, diff, and extend — and required when using Rule objects or closures.
// WRONG:
'email' => 'required|email|unique:users',
// CORRECT:
'email' => ['required', 'email', 'unique:users'],
Quick Decision Guide
"How do I validate?"
| Scenario | Approach |
|---|---|
| Simple controller validation | $request->validate([...]) |
| Reusable/complex validation | Form Request class |
| Outside HTTP request (CLI, jobs) | Validator::make($data, $rules) |
| Validate and get safe data | $request->safe() for ValidatedInput object |
| Validate but keep going | Validator::make()->validate() or check ->fails() |
"Which rule do I need?"
Presence & optionality:
| Need | Rule |
|---|---|
| Must be present and non-empty | required |
| Required only if present | sometimes |
| May be null | nullable |
| Must be present (even if empty) | present |
| Must not be in input | missing |
| Required when other field has value | required_if:field,value |
| Required when other field is absent | required_without:field |
| Exclude from validated output | exclude |
Type constraints:
| Need | Rule |
|---|---|
| String | string |
| Integer | integer |
| Numeric (int, float, string) | numeric |
| Boolean | boolean |
| Array | array |
| Sequential list (0-indexed) | list |
| Date | date or date_format:Y-m-d |
| File upload | file |
| Image | image |
email |
|
| URL | url |
| JSON | json |
| UUID | uuid |
Size & range:
| Need | Rule |
|---|---|
| Min/max length, value, count, or KB | min:n / max:n |
| Exact size | size:n |
| Between range | between:min,max |
| Compare to another field | gt:field / gte:field / lt:field / lte:field |
Database:
| Need | Rule |
|---|---|
| Must exist in table | exists:table,column or Rule::exists(...) |
| Must be unique in table | unique:table,column or Rule::unique(...) |
Validated Data Access
// Get only validated fields
$validated = $request->validated();
// Get ValidatedInput object (supports only/except/merge/etc.)
$safe = $request->safe();
$safe->only(['name', 'email']);
$safe->except(['password']);
$safe->merge(['ip' => $request->ip()]);
Common Pitfalls
1. Missing nullable on optional fields
Laravel's ConvertEmptyStringsToNull middleware converts "" to null. Without nullable, empty optional fields fail validation.
// WRONG: empty string becomes null, fails 'string' rule
'bio' => ['string', 'max:500'],
// CORRECT:
'bio' => ['nullable', 'string', 'max:500'],
2. sometimes vs nullable vs required
// required: MUST be present and non-empty
'name' => ['required', 'string'],
// nullable: can be present as null
'nickname' => ['nullable', 'string'],
// sometimes: only validate IF the field is in the input at all
// (useful for PATCH requests where fields are optional)
'email' => ['sometimes', 'required', 'email'],
3. Unsafe unique ignore
Never pass user-controlled input to ignore():
// DANGEROUS: SQL injection risk
Rule::unique('users')->ignore($request->input('id'))
// SAFE: use the authenticated model
Rule::unique('users')->ignore($user->id)
4. bail placement
bail must be the first rule to stop processing on first failure:
// WRONG: bail has no effect here
'email' => ['required', 'bail', 'email', 'unique:users'],
// CORRECT: bail first
'email' => ['bail', 'required', 'email', 'unique:users'],
5. Array vs nested validation confusion
// Validates the array itself
'tags' => ['required', 'array', 'min:1'],
// Validates each item IN the array
'tags.*' => ['string', 'max:50'],
// Nested objects in array
'users.*.email' => ['required', 'email'],
'users.*.roles.*' => ['exists:roles,name'],
6. exists / unique with soft deletes
// Ignores soft-deleted records (default counts them as existing)
Rule::unique('users', 'email')->withoutTrashed()
// Only check non-deleted records exist
Rule::exists('users', 'id')->whereNull('deleted_at')
7. Date comparison with field references
// Compare against another field, not a literal date
'end_date' => ['required', 'date', 'after:start_date'],
// Compare against a literal date
'start_date' => ['required', 'date', 'after:2024-01-01'],
// Use after_or_equal when same-day should be valid
'checkout' => ['required', 'date', 'after_or_equal:checkin'],
Rule Builders
Use fluent builders instead of string rules for complex validation. See references/rule-builders.md for the full API of every builder.
Password
use Illuminate\Validation\Rules\Password;
// Define defaults (typically in AppServiceProvider::boot)
Password::defaults(fn () => Password::min(8)->uncompromised());
// Use in rules
'password' => ['required', 'confirmed', Password::default()],
// Full chain
'password' => ['required', Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(3)], // min 3 appearances in breach DB
File
use Illuminate\Validation\Rules\File;
'document' => ['required', File::types(['pdf', 'docx'])->max('10mb')],
'avatar' => ['required', Rule::imageFile()->dimensions(Rule::dimensions()->maxWidth(2000))],
Enum
'status' => [Rule::enum(OrderStatus::class)->except([OrderStatus::Cancelled])],
Date & Numeric
'start_date' => [Rule::date()->afterToday()->format('Y-m-d')],
'price' => [Rule::numeric()->decimal(2)->min(0)->max(99999.99)],
'email' => [Rule::email()->rfcCompliant()->validateMxRecord()],
Conditional Validation
use Illuminate\Validation\Rule;
$request->validate([
'role' => ['required', 'string'],
// Required only when role is admin
'admin_code' => [Rule::requiredIf($request->role === 'admin')],
// Using closures for complex conditions
'company' => [Rule::requiredIf(fn () => $user->isBusinessAccount())],
// Exclude from output when condition met
'coupon' => ['exclude_if:type,free', 'required', 'string'],
]);
After validation hooks
$validator = Validator::make($data, $rules);
$validator->after(function ($validator) {
if ($this->somethingElseIsInvalid()) {
$validator->errors()->add('field', 'Something is wrong.');
}
});
Custom Rules
Rule object (recommended)
php artisan make:rule Uppercase
php artisan make:rule Uppercase --implicit # runs even on empty values
class Uppercase implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (strtoupper($value) !== $value) {
$fail('The :attribute must be uppercase.');
}
}
}
// Usage
'name' => ['required', new Uppercase],
Closure rule (one-off)
'title' => [
'required',
function (string $attribute, mixed $value, Closure $fail) {
if ($value === 'foo') {
$fail("The {$attribute} is invalid.");
}
},
],
Custom Error Messages
// Inline
$request->validate([
'email' => ['required', 'email'],
], [
'email.required' => 'We need your email address.',
'email.email' => 'That doesn\'t look like a valid email.',
]);
// In Form Request
public function messages(): array
{
return [
'title.required' => 'A title is required.',
'body.required' => 'A message body is required.',
];
}
// Custom attribute names
public function attributes(): array
{
return [
'email' => 'email address',
];
}
// Array position placeholder
'photos.*.description.required' => 'Please describe photo #:position.',
Reference Files
- All validation rules: See references/rules.md for every built-in rule with signatures and descriptions
- Rule class & fluent builders: See references/rule-builders.md for
Rule::unique(),Rule::exists(),Password::,File::,Rule::date(),Rule::numeric(),Rule::email(),Rule::enum(),Rule::dimensions(), and all other fluent builder APIs - Form Requests: See references/form-requests.md for Form Request patterns, methods, and advanced usage
Weekly Installs
3
Repository
riasvdv/skillsGitHub Stars
3
First Seen
Feb 16, 2026
Security Audits
Installed on
opencode3
gemini-cli3
claude-code3
github-copilot3
windsurf3
codex3