Advanced Laravel
Advanced Validation & Form Requests
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.
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.
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.
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.