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.mdsource 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?"
| Scenario | Approach |
|---|---|
| Simple controller validation | |
| Reusable/complex validation | Form Request class |
| Outside HTTP request (CLI, jobs) | |
| Validate and get safe data | for object |
| Validate but keep going | or check |
"Which rule do I need?"
Presence & optionality:
| Need | Rule |
|---|---|
| Must be present and non-empty | |
| Required only if present | |
| May be null | |
| Must be present (even if empty) | |
| Must not be in input | |
| Required when other field has value | |
| Required when other field is absent | |
| Exclude from validated output | |
Type constraints:
| Need | Rule |
|---|---|
| String | |
| Integer | |
| Numeric (int, float, string) | |
| Boolean | |
| Array | |
| Sequential list (0-indexed) | |
| Date | or |
| File upload | |
| Image | |
| |
| URL | |
| JSON | |
| UUID | |
Size & range:
| Need | Rule |
|---|---|
| Min/max length, value, count, or KB | / |
| Exact size | |
| Between range | |
| Compare to another field | / / / |
Database:
| Need | Rule |
|---|---|
| Must exist in table | or |
| Must be unique in table | or |
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
nullableLaravel'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
sometimesnullablerequired// 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
uniqueNever 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
bailbail 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
existsunique// 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()
, and all other fluent builder APIsRule::dimensions() - Form Requests: See references/form-requests.md for Form Request patterns, methods, and advanced usage