Skillshub laravel-validation

Laravel Validation

install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/riasvdv/skills/laravel-validation" ~/.claude/skills/comeonoliver-skillshub-laravel-validation && rm -rf "$T"
manifest: skills/riasvdv/skills/laravel-validation/SKILL.md
source content

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?"

ScenarioApproach
Simple controller validation
$request->validate([...])
Reusable/complex validationForm 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:

NeedRule
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:

NeedRule
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
email
URL
url
JSON
json
UUID
uuid

Size & range:

NeedRule
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:

NeedRule
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