Advanced Laravel

Advanced Validation & Form Requests

18 min Lesson 6 of 40

Advanced Validation & Form Requests

Master Laravel's advanced validation techniques including complex rules, conditional validation, custom validators, and form request optimization.

Complex Validation Rules

Laravel provides powerful validation rules that go beyond basic checks:

use Illuminate\Validation\Rule; // Unique with multiple columns 'email' => [ 'required', Rule::unique('users') ->where('account_id', $accountId) ->ignore($userId) ->whereNull('deleted_at') ], // Conditional exists check 'parent_id' => [ 'nullable', Rule::exists('categories', 'id') ->where('type', 'parent') ->whereNull('archived_at') ], // In with database values 'status' => [ 'required', Rule::in(Status::pluck('code')->toArray()) ], // Custom unique rule with encryption 'username' => [ 'required', Rule::unique('users') ->using(function ($query) { return $query->where('tenant_id', auth()->user()->tenant_id); }) ]
Pro Tip: Use Rule objects instead of pipe-separated strings for complex validation. They provide better readability and IDE autocompletion.

Conditional Validation

Apply validation rules based on conditions:

use Illuminate\Validation\Rule; public function rules() { return [ 'email' => 'required|email', 'role' => 'required|in:user,admin,manager', // Validate only if role is admin 'permissions' => [ Rule::requiredIf($this->role === 'admin'), 'array' ], 'permissions.*' => 'exists:permissions,id', // Validate based on another field 'company_name' => [ Rule::requiredIf(function () { return $this->account_type === 'business'; }) ], // Exclude validation if condition met 'vat_number' => [ Rule::excludeIf($this->country !== 'UK'), 'required', 'regex:/^GB[0-9]{9}$/' ], // Prohibit field based on condition 'discount_code' => [ Rule::prohibitedIf($this->user()->hasActiveSubscription()), 'string' ] ]; } // Sometimes validation public function rules() { $rules = [ 'name' => 'required', 'email' => 'required|email' ]; // Add rules conditionally if ($this->has('is_premium')) { $rules['subscription_plan'] = 'required|exists:plans,id'; $rules['payment_method'] = 'required|in:card,paypal'; } return $rules; }

Custom Validation Rules

Create reusable custom validation rules:

// Generate rule class php artisan make:rule Uppercase // app/Rules/Uppercase.php namespace App\Rules; use Illuminate\Contracts\Validation\ValidationRule; class Uppercase implements ValidationRule { public function validate(string $attribute, mixed $value, Closure $fail): void { if (strtoupper($value) !== $value) { $fail('The :attribute must be uppercase.'); } } } // Advanced custom rule with parameters class PhoneNumber implements ValidationRule { public function __construct( private string $countryCode = 'US' ) {} public function validate(string $attribute, mixed $value, Closure $fail): void { $pattern = match($this->countryCode) { 'US' => '/^\+1[0-9]{10}$/', 'UK' => '/^\+44[0-9]{10}$/', 'SA' => '/^\+966[0-9]{9}$/', default => '/^\+[0-9]{10,15}$/' }; if (!preg_match($pattern, $value)) { $fail("The :attribute must be a valid {$this->countryCode} phone number."); } } } // Usage in form request public function rules() { return [ 'name' => ['required', new Uppercase], 'phone' => ['required', new PhoneNumber('SA')] ]; }
Note: Custom rules are invoked objects, making them more flexible than closure-based validation. They support dependency injection and are easier to test.

After Validation Hooks

Execute logic after validation passes:

namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StoreProductRequest extends FormRequest { public function rules() { return [ 'name' => 'required|string|max:255', 'price' => 'required|numeric|min:0', 'category_id' => 'required|exists:categories,id' ]; } // Execute after validation passes protected function passedValidation() { // Merge additional data $this->merge([ 'slug' => Str::slug($this->name), 'user_id' => auth()->id(), 'published_at' => now() ]); // Transform data $this->merge([ 'price' => $this->price * 100, // Convert to cents 'tags' => array_map('trim', explode(',', $this->tags)) ]); } // Execute after validator created but before validation public function withValidator($validator) { $validator->after(function ($validator) { // Cross-field validation if ($this->start_date > $this->end_date) { $validator->errors()->add( 'end_date', 'End date must be after start date.' ); } // Business logic validation if ($this->discount > 0 && !auth()->user()->canApplyDiscounts()) { $validator->errors()->add( 'discount', 'You don\'t have permission to apply discounts.' ); } }); } }

Nested Validation

Validate complex nested data structures:

public function rules() { return [ // Array validation 'items' => 'required|array|min:1|max:10', 'items.*.name' => 'required|string|max:100', 'items.*.quantity' => 'required|integer|min:1', 'items.*.price' => 'required|numeric|min:0', // Nested object validation 'shipping' => 'required|array', 'shipping.address' => 'required|string', 'shipping.city' => 'required|string', 'shipping.postal_code' => 'required|string', // Multi-level nesting 'users' => 'array', 'users.*.name' => 'required|string', 'users.*.email' => 'required|email', 'users.*.roles' => 'array', 'users.*.roles.*.name' => 'required|exists:roles,name', 'users.*.roles.*.permissions' => 'array', 'users.*.roles.*.permissions.*' => 'exists:permissions,id', // Dynamic keys validation 'metadata' => 'array', 'metadata.*' => 'string|max:500' ]; } // Custom nested validation public function withValidator($validator) { $validator->after(function ($validator) { $total = collect($this->items) ->sum(fn($item) => $item['quantity'] * $item['price']); if ($total > 10000) { $validator->errors()->add( 'items', 'Total order value cannot exceed $10,000.' ); } }); }
Warning: Nested validation can impact performance on large datasets. Consider validating in batches or using database constraints for bulk operations.

Advanced Form Request Techniques

Optimize form requests with advanced patterns:

namespace App\Http\Requests; class UpdateUserRequest extends FormRequest { // Authorize with policies public function authorize() { return $this->user()->can('update', $this->route('user')); } // Dynamic rules based on user role public function rules() { $rules = [ 'name' => 'required|string|max:255', 'email' => [ 'required', 'email', Rule::unique('users')->ignore($this->route('user')) ] ]; // Admins can change roles if ($this->user()->isAdmin()) { $rules['role'] = 'required|exists:roles,name'; } return $rules; } // Custom error messages public function messages() { return [ 'email.unique' => 'This email is already taken by another user.', 'name.required' => 'Please provide a name for the user.' ]; } // Custom attribute names public function attributes() { return [ 'email' => 'email address', 'phone' => 'phone number' ]; } // Prepare input before validation protected function prepareForValidation() { $this->merge([ 'slug' => Str::slug($this->name), 'phone' => preg_replace('/[^0-9+]/', '', $this->phone) ]); } }

Validation Error Handling

Customize how validation errors are returned:

// Return custom error format protected function failedValidation(Validator $validator) { throw new HttpResponseException( response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $validator->errors()->toArray(), 'timestamp' => now()->toIso8601String() ], 422) ); } // Log validation failures protected function failedValidation(Validator $validator) { Log::warning('Validation failed', [ 'user_id' => auth()->id(), 'url' => $this->fullUrl(), 'errors' => $validator->errors()->toArray() ]); parent::failedValidation($validator); } // Custom authorization failure protected function failedAuthorization() { throw new HttpResponseException( response()->json([ 'message' => 'You are not authorized to perform this action.' ], 403) ); }
Exercise 1: Create a form request for creating an invoice with nested line items. Validate that:
1. Invoice has customer_id, issue_date, due_date
2. Due date is after issue date
3. Line items array has at least one item
4. Each item has description, quantity (min 1), unit_price (min 0)
5. Total amount doesn't exceed $50,000
Use after validation hooks to calculate total and generate invoice number.
Exercise 2: Create a custom validation rule "StrongPassword" that requires:
1. Minimum 12 characters
2. At least one uppercase letter
3. At least one lowercase letter
4. At least one number
5. At least one special character
6. Not in common passwords list (check against array)
Implement with clear error messages for each failed requirement.
Exercise 3: Build a multi-step form request that validates user registration data:
Step 1: Email, password, password_confirmation
Step 2: Name, phone, date_of_birth
Step 3: Address, city, postal_code, country
Use conditional validation based on a "step" field. Store intermediate data in session between steps.