Laravel Validation Rules and Customization

Module 19: PHP Backend - Laravel

Introduction to Laravel Validation

Data validation is a critical aspect of web application development. It ensures that incoming data meets your application's requirements before processing it. Laravel provides a robust, flexible validation system that makes it easy to validate incoming requests while keeping your code clean and organized.

Think of validation as the security checkpoint for your application's data. Just as an airport security checkpoint prevents prohibited items from entering, validation prevents invalid or malicious data from entering your application.

graph TD A[User Input] --> B[Validation Layer] B -->|Valid| C[Application Logic] B -->|Invalid| D[Error Response] C --> E[Database] D --> F[Form with Errors]

In the previous lecture, we introduced basic validation concepts. In this lecture, we'll delve deeper into Laravel's validation capabilities, exploring the wide range of built-in validation rules and how to customize them for your specific needs.

Validation Basics Review

Validation in Controllers


public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'body' => 'required|string|min:10',
        'published_at' => 'nullable|date',
    ]);
    
    // Validation passed; proceed with creating the resource
    $post = Post::create($validated);
    
    return redirect()->route('posts.show', $post);
}
            

Using Form Request Classes


// Generate a Form Request
php artisan make:request StorePostRequest

// app/Http/Requests/StorePostRequest.php
class StorePostRequest extends FormRequest
{
    public function authorize()
    {
        return true; // or use auth logic
    }
    
    public function rules()
    {
        return [
            'title' => 'required|string|max:255',
            'body' => 'required|string|min:10',
            'published_at' => 'nullable|date',
        ];
    }
}

// In the controller
public function store(StorePostRequest $request)
{
    // Validation already happened
    $post = Post::create($request->validated());
    
    return redirect()->route('posts.show', $post);
}
            

These methods provide a foundation for validation in Laravel, but the system offers much more depth and flexibility than just these basics.

Available Validation Rules

Laravel provides an extensive set of validation rules. Here's an overview categorized by type:

Basic Validation Rules

String and Text Validation

Numeric Validation

Date Validation

Array and Object Validation

File Validation

Database Related Validation

These rules can be combined to create sophisticated validation requirements for your forms. Think of them as building blocks for constructing a comprehensive validation strategy.

Advanced Rule Usage

Array Validation


// Validating a simple array
$rules = [
    'tags' => 'required|array|min:1|max:5',
    'tags.*' => 'string|max:50',
];

// Validating a nested array
$rules = [
    'users' => 'required|array|min:1',
    'users.*.name' => 'required|string|max:255',
    'users.*.email' => 'required|email|unique:users,email',
    'users.*.roles' => 'array',
    'users.*.roles.*' => 'exists:roles,id',
];
            

The * notation allows you to validate each element of an array. This is particularly useful for forms that allow adding multiple items, like a product with multiple variations.

Custom Rule Arrays


// Using arrays for validation rules
$rules = [
    'email' => ['required', 'email', 'max:255', 'unique:users'],
    'password' => ['required', 'min:8', 'confirmed'],
];

// With conditional rules
$rules = [
    'email' => ['required', 'email', 'max:255'],
    'role' => ['required', 'in:admin,user,editor'],
    'permissions' => [
        'required_if:role,admin',
        'array',
    ],
];
            

Using arrays for rules makes your validation code more readable, especially for fields with many rules.

Rule Objects

For more complex validation rules, Laravel supports using rule objects:


use Illuminate\Validation\Rules\Password;

$rules = [
    'password' => [
        'required',
        Password::min(8)
            ->letters()
            ->mixedCase()
            ->numbers()
            ->symbols()
            ->uncompromised(),
    ],
];
            

The Password rule class provides a fluent interface for defining password requirements, including checking if the password has been compromised in data breaches using the Have I Been Pwned API.

File Validation Examples


// Basic file validation
$rules = [
    'photo' => 'required|file|image|max:2048', // 2MB max
];

// Specific file types
$rules = [
    'document' => 'required|file|mimes:pdf,doc,docx|max:10240', // 10MB max
];

// Image dimensions
$rules = [
    'banner' => [
        'required',
        'image',
        'dimensions:min_width=1200,min_height=300,max_width=2400,max_height=600',
    ],
];

// Multiple files
$rules = [
    'photos' => 'required|array|min:1|max:5',
    'photos.*' => 'image|max:2048',
];
            

These file validation rules help ensure that uploaded files meet your requirements for type, size, and dimensions before you attempt to process them.

Database Rules


// Basic exists and unique
$rules = [
    'email' => 'required|email|unique:users,email',
    'category_id' => 'required|exists:categories,id',
];

// Unique with exceptions (for updates)
$rules = [
    'email' => 'required|email|unique:users,email,' . $user->id,
];

// More complex unique rule
$rules = [
    'username' => [
        'required',
        Rule::unique('users')->ignore($user->id)->where(function ($query) {
            return $query->where('active', true);
        }),
    ],
];

// Exists with constraints
$rules = [
    'category_id' => [
        'required',
        Rule::exists('categories', 'id')->where(function ($query) {
            return $query->where('active', true);
        }),
    ],
];
            

Database rules are essential for maintaining data integrity in your application. They ensure that referenced IDs exist and that unique constraints are respected.

Conditionally Adding Rules

Sometimes you need to apply validation rules conditionally based on other input data or application state:

Using sometimes


$rules = [
    'middle_name' => 'sometimes|string|max:255',
];
            

The sometimes rule means the field will only be validated if it's present in the input data.

Using Validator::sometimes()


$validator = Validator::make($request->all(), [
    'email' => 'required|email',
    'games' => 'required|numeric',
]);

$validator->sometimes('reason', 'required|max:500', function ($input) {
    return $input->games >= 100;
});
            

This approach allows for more complex conditional logic. Here, the 'reason' field is only required when the 'games' value is 100 or more.

Using exclude_if / exclude_unless


$rules = [
    'payment_type' => 'required|in:credit,paypal,bank',
    'card_number' => 'exclude_if:payment_type,paypal,bank|required|string',
    'paypal_email' => 'exclude_unless:payment_type,paypal|required|email',
    'bank_account' => 'exclude_unless:payment_type,bank|required|string',
];
            

These rules help manage interdependent fields by excluding fields from validation based on conditions.

Building Rules Dynamically


public function rules()
{
    $rules = [
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users,email',
    ];
    
    if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
        $userId = $this->route('user');
        $rules['email'] = "required|email|unique:users,email,{$userId}";
        $rules['password'] = 'sometimes|required|min:8|confirmed';
    } else {
        $rules['password'] = 'required|min:8|confirmed';
    }
    
    if ($this->input('role') === 'admin') {
        $rules['permissions'] = 'required|array';
        $rules['permissions.*'] = 'exists:permissions,id';
    }
    
    return $rules;
}
            

This approach allows you to build complex validation rules based on multiple conditions, such as the request method, route parameters, and input values.

Creating Custom Validation Rules

Laravel offers several ways to create custom validation rules when the built-in rules don't meet your needs:

Using Rule Objects


// Generate a rule object
php artisan make:rule StrongPassword

// app/Rules/StrongPassword.php
namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class StrongPassword implements Rule
{
    public function passes($attribute, $value)
    {
        // Value must have at least 8 characters
        if (strlen($value) < 8) {
            return false;
        }
        
        // Value must contain at least one uppercase letter
        if (!preg_match('/[A-Z]/', $value)) {
            return false;
        }
        
        // Value must contain at least one lowercase letter
        if (!preg_match('/[a-z]/', $value)) {
            return false;
        }
        
        // Value must contain at least one number
        if (!preg_match('/[0-9]/', $value)) {
            return false;
        }
        
        // Value must contain at least one special character
        if (!preg_match('/[^A-Za-z0-9]/', $value)) {
            return false;
        }
        
        return true;
    }

    public function message()
    {
        return 'The :attribute must be at least 8 characters and include uppercase, lowercase, numbers, and special characters.';
    }
}

// Using the rule
$request->validate([
    'password' => ['required', new StrongPassword],
]);
            

Rule objects are ideal for complex validation logic that you want to reuse across your application. They encapsulate both the validation logic and the error message.

Using Closure Rules


$rules = [
    'password' => [
        'required',
        function ($attribute, $value, $fail) {
            if (strlen($value) < 8) {
                $fail('The '.$attribute.' must be at least 8 characters.');
            }
            
            if (!preg_match('/[A-Z]/', $value)) {
                $fail('The '.$attribute.' must contain at least one uppercase letter.');
            }
            
            // More checks...
        },
    ],
];
            

Closure rules are convenient for one-off validations that are specific to a particular form or use case.

Using Validator::extend()


// In a service provider's boot method
Validator::extend('strong_password', function ($attribute, $value, $parameters, $validator) {
    return strlen($value) >= 8 &&
           preg_match('/[A-Z]/', $value) &&
           preg_match('/[a-z]/', $value) &&
           preg_match('/[0-9]/', $value) &&
           preg_match('/[^A-Za-z0-9]/', $value);
});

// Define custom error message
Validator::replacer('strong_password', function ($message, $attribute, $rule, $parameters) {
    return "The {$attribute} must be at least 8 characters and include uppercase, lowercase, numbers, and special characters.";
});

// Using the rule
$rules = [
    'password' => 'required|strong_password',
];
            

This approach registers a custom rule globally, making it available throughout your application like any built-in rule.

Invokable Rule Classes

In Laravel 9+, you can use invokable rule classes:


// Generate an invokable rule
php artisan make:rule StrongPassword --invokable

// app/Rules/StrongPassword.php
namespace App\Rules;

use Illuminate\Contracts\Validation\InvokableRule;

class StrongPassword implements InvokableRule
{
    public function __invoke($attribute, $value, $fail)
    {
        if (strlen($value) < 8) {
            $fail('The '.$attribute.' must be at least 8 characters.');
        }
        
        if (!preg_match('/[A-Z]/', $value)) {
            $fail('The '.$attribute.' must contain at least one uppercase letter.');
        }
        
        // More checks...
    }
}

// Using the rule
$rules = [
    'password' => ['required', new StrongPassword],
];
            

Invokable rules combine the maintainability of rule objects with the simplicity of closure rules.

Customizing Error Messages

Laravel provides several ways to customize validation error messages:

Custom Messages in validate()


$validated = $request->validate([
    'title' => 'required|max:255',
    'body' => 'required',
], [
    'title.required' => 'A title is required',
    'title.max' => 'Title cannot be more than 255 characters',
    'body.required' => 'A message is required',
]);
            

Custom Messages in Form Requests


class StorePostRequest extends FormRequest
{
    public function rules()
    {
        return [
            'title' => 'required|max:255',
            'body' => 'required',
        ];
    }
    
    public function messages()
    {
        return [
            'title.required' => 'A title is required',
            'title.max' => 'Title cannot be more than 255 characters',
            'body.required' => 'A message is required',
        ];
    }
    
    // For specific attributes
    public function attributes()
    {
        return [
            'email' => 'email address',
        ];
    }
}
            

Custom Messages for Specific Fields


$messages = [
    'required' => 'The :attribute field is required',
    'email.required' => 'We need your email address',
    'password.min' => 'Password should be at least :min characters',
];

$validator = Validator::make($request->all(), $rules, $messages);
            

You can use placeholders in your messages to make them more dynamic:

Localized Messages

For multi-language applications, you can translate validation messages:


// resources/lang/en/validation.php
return [
    'required' => 'The :attribute field is required.',
    'email' => 'The :attribute must be a valid email address.',
    // ...
    
    'custom' => [
        'email' => [
            'required' => 'We need your email address',
        ],
    ],
    
    'attributes' => [
        'email' => 'email address',
    ],
];
            

Laravel will use these translations when generating validation error messages.

Manually Creating Validators

While the validate() method is convenient, sometimes you need more control over the validation process:


use Illuminate\Support\Facades\Validator;

public function store(Request $request)
{
    $validator = Validator::make($request->all(), [
        'title' => 'required|max:255',
        'body' => 'required',
    ]);
    
    if ($validator->fails()) {
        return redirect('post/create')
            ->withErrors($validator)
            ->withInput();
    }
    
    // Validation passed
    $validated = $validator->validated();
    
    // Create the post
    $post = Post::create($validated);
    
    return redirect()->route('posts.show', $post);
}
            

The manual approach gives you more flexibility, such as adding conditional validation rules or performing actions even when validation fails.

Advanced Validator Methods


// Get all validation errors
$errors = $validator->errors();

// Get first error for a field
$firstError = $validator->errors()->first('email');

// Check if a specific field has errors
if ($validator->errors()->has('email')) {
    // ...
}

// Get validated data
$validated = $validator->validated();

// Get parsed rules
$rules = $validator->getRules();

// Add custom errors
$validator->errors()->add('field', 'Custom error message');

// Execute validation for specific fields only
$validator->only(['name', 'email']);

// Skip validation for specific fields
$validator->except(['password', 'password_confirmation']);

// Check validation status without redirecting
if ($validator->fails()) {
    $errors = $validator->errors();
    // Process errors without redirection
}
            

These methods give you fine-grained control over the validation process.

After Validation Hook


$validator = Validator::make($request->all(), $rules);

$validator->after(function ($validator) use ($request) {
    // Perform complex validation that needs to happen after all other rules
    if ($request->input('password') === '123456') {
        $validator->errors()->add('password', 'Your password cannot be 123456');
    }
    
    // Check database conditions
    if (User::where('email', $request->email)->where('is_banned', true)->exists()) {
        $validator->errors()->add('email', 'This account has been banned');
    }
});

if ($validator->fails()) {
    // Handle validation errors
}
            

The after() hook is perfect for validation logic that depends on multiple fields or requires database queries.

API Validation

When building APIs, you typically want to return validation errors as JSON instead of redirecting:


// In a controller
public function store(Request $request)
{
    try {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8',
        ]);
        
        $user = User::create($validated);
        
        return response()->json([
            'message' => 'User created successfully',
            'user' => $user
        ], 201);
    } catch (ValidationException $e) {
        return response()->json([
            'message' => 'Validation failed',
            'errors' => $e->errors()
        ], 422);
    }
}
            

However, Laravel automatically handles this for you when the request expects JSON:


// If the request wants a JSON response (has Accept: application/json header)
// Laravel will automatically return a JSON response with validation errors
public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users',
        'password' => 'required|min:8',
    ]);
    
    $user = User::create($validated);
    
    return response()->json([
        'message' => 'User created successfully',
        'user' => $user
    ], 201);
}
            

When validation fails, Laravel will return a JSON response like this:


{
    "message": "The given data was invalid.",
    "errors": {
        "name": [
            "The name field is required."
        ],
        "email": [
            "The email field is required."
        ]
    }
}
            

Customizing API Validation Responses


// In a Form Request class
protected function failedValidation(Validator $validator)
{
    throw new HttpResponseException(
        response()->json([
            'success' => false,
            'message' => 'Validation errors',
            'data' => $validator->errors()
        ], 422)
    );
}
            

This allows you to customize the format of API validation error responses to match your API's conventions.

Form Validation with JavaScript

While server-side validation is essential, client-side validation provides immediate feedback to users. Laravel works well with JavaScript validation libraries:

HTML5 Validation Attributes


<!-- Using HTML5 attributes for client-side validation -->
<input type="email" name="email" required minlength="5" maxlength="255">
<input type="password" name="password" required minlength="8">
<input type="number" name="age" min="18" max="120">
<input type="url" name="website">
            

HTML5 validation provides basic client-side validation but should always be backed by server-side validation.

Laravel & JavaScript Validation

One approach is to pass validation rules to JavaScript:


<!-- In your Blade view -->
<form id="createUserForm" method="POST" action="{{ route('users.store') }}">
    @csrf
    
    <div class="form-group">
        <input type="text" name="name" id="name" value="{{ old('name') }}">
    </div>
    
    <div class="form-group">
        <input type="email" name="email" id="email" value="{{ old('email') }}">
    </div>
    
    <button type="submit">Create User</button>
</form>

<script>
    // Pass Laravel validation rules to JavaScript
    const validationRules = @json([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users',
    ]);
    
    // Then use a JS validation library to apply these rules
    // (This is pseudocode - implementation depends on your validation library)
    setupValidator('createUserForm', validationRules);
</script>
            

This approach ensures your client-side and server-side validation rules stay in sync.

Using Laravel Form Request in JavaScript


// routes/web.php
Route::get('/validation-rules/{formRequest}', function ($formRequest) {
    $class = "App\\Http\\Requests\\{$formRequest}";
    
    if (!class_exists($class)) {
        return response()->json(['error' => 'Form request not found'], 404);
    }
    
    $request = new $class;
    
    return response()->json([
        'rules' => $request->rules(),
        'messages' => method_exists($request, 'messages') ? $request->messages() : [],
    ]);
})->middleware('auth');

// In your JavaScript
fetch('/validation-rules/StoreUserRequest')
    .then(response => response.json())
    .then(data => {
        // Initialize client-side validation with these rules
        setupValidator('form', data.rules, data.messages);
    });
            

This more advanced approach lets you reuse your Form Request validation rules in your JavaScript code.

Real-World Example: Product Management Form

Let's implement a comprehensive product management form with complex validation requirements:

Form Request


// app/Http/Requests/StoreProductRequest.php
namespace App\Http\Requests;

use App\Rules\Barcode;
use Illuminate\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;

class StoreProductRequest extends FormRequest
{
    public function authorize()
    {
        return $this->user()->can('create', Product::class);
    }
    
    public function rules()
    {
        return [
            // Basic Product Information
            'name' => 'required|string|max:255',
            'slug' => [
                'nullable', 
                'string', 
                'max:255', 
                Rule::unique('products')->ignore($this->product),
                'regex:/^[a-z0-9\-]+$/',
            ],
            'description' => 'required|string|min:20',
            'short_description' => 'nullable|string|max:255',
            
            // Pricing and Inventory
            'price' => 'required|numeric|min:0.01|max:999999.99',
            'sale_price' => 'nullable|numeric|min:0.01|lt:price',
            'cost' => 'nullable|numeric|min:0',
            'sku' => [
                'required', 
                'string', 
                'max:50', 
                Rule::unique('products')->ignore($this->product),
            ],
            'barcode' => ['nullable', 'string', new Barcode],
            'quantity' => 'required|integer|min:0',
            'backorder' => 'boolean',
            'requires_shipping' => 'boolean',
            
            // Categorization
            'category_id' => 'required|exists:categories,id',
            'tags' => 'nullable|array',
            'tags.*' => 'exists:tags,id',
            
            // Variants
            'has_variants' => 'boolean',
            'variants' => 'required_if:has_variants,true|array|min:1',
            'variants.*.name' => 'required_with:variants|string|max:255',
            'variants.*.sku' => [
                'required_with:variants',
                'string',
                'max:50',
                Rule::unique('product_variants', 'sku')->where(function ($query) {
                    $query->where('product_id', '!=', $this->product?->id);
                }),
            ],
            'variants.*.price' => 'required_with:variants|numeric|min:0.01',
            'variants.*.quantity' => 'required_with:variants|integer|min:0',
            
            // Media
            'featured_image' => 'nullable|image|max:2048|dimensions:min_width=800,min_height=800',
            'gallery' => 'nullable|array|max:5',
            'gallery.*' => 'image|max:2048',
            
            // SEO
            'seo_title' => 'nullable|string|max:60',
            'seo_description' => 'nullable|string|max:160',
            'seo_keywords' => 'nullable|string|max:255',
            
            // Shipping
            'weight' => 'nullable|numeric|min:0',
            'length' => 'nullable|numeric|min:0',
            'width' => 'nullable|numeric|min:0',
            'height' => 'nullable|numeric|min:0',
            
            // Attributes
            'attributes' => 'nullable|array',
            'attributes.*.name' => 'required_with:attributes|string|max:100',
            'attributes.*.value' => 'required_with:attributes|string|max:255',
        ];
    }
    
    public function messages()
    {
        return [
            'name.required' => 'Product name is required',
            'slug.unique' => 'This product slug is already in use',
            'slug.regex' => 'Slug may only contain lowercase letters, numbers, and hyphens',
            'price.required' => 'Product price is required',
            'price.min' => 'Price must be at least :min',
            'sale_price.lt' => 'Sale price must be less than regular price',
            'sku.required' => 'SKU is required',
            'sku.unique' => 'This SKU is already in use',
            'category_id.required' => 'Please select a product category',
            'category_id.exists' => 'The selected category is invalid',
            'variants.required_if' => 'At least one variant is required when product has variants',
            'variants.*.sku.unique' => 'Variant SKU must be unique',
            'featured_image.dimensions' => 'Featured image must be at least 800x800 pixels',
        ];
    }
    
    public function attributes()
    {
        return [
            'category_id' => 'category',
            'variants.*.name' => 'variant name',
            'variants.*.sku' => 'variant SKU',
            'variants.*.price' => 'variant price',
            'seo_title' => 'SEO title',
            'seo_description' => 'SEO description',
        ];
    }
    
    protected function prepareForValidation()
    {
        // Generate slug from name if not provided
        if (!$this->slug && $this->name) {
            $this->merge([
                'slug' => \Str::slug($this->name),
            ]);
        }
        
        // Set default values for booleans
        $this->merge([
            'backorder' => $this->boolean('backorder'),
            'requires_shipping' => $this->boolean('requires_shipping'),
            'has_variants' => $this->boolean('has_variants'),
        ]);
    }
}
            

Custom Barcode Rule


// app/Rules/Barcode.php
namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class Barcode implements Rule
{
    public function passes($attribute, $value)
    {
        // Skip validation if empty
        if (empty($value)) {
            return true;
        }
        
        // UPC-A: Must be 12 digits
        if (strlen($value) === 12 && ctype_digit($value)) {
            return $this->validateUpcA($value);
        }
        
        // EAN-13: Must be 13 digits
        if (strlen($value) === 13 && ctype_digit($value)) {
            return $this->validateEan13($value);
        }
        
        // Not a recognized barcode format
        return false;
    }
    
    protected function validateUpcA($barcode)
    {
        // UPC-A check digit validation
        $sum = 0;
        
        for ($i = 0; $i < 11; $i++) {
            $sum += $i % 2 === 0 ? $barcode[$i] * 3 : $barcode[$i];
        }
        
        $checkDigit = (10 - ($sum % 10)) % 10;
        
        return $checkDigit === (int) $barcode[11];
    }
    
    protected function validateEan13($barcode)
    {
        // EAN-13 check digit validation
        $sum = 0;
        
        for ($i = 0; $i < 12; $i++) {
            $sum += $i % 2 === 0 ? $barcode[$i] : $barcode[$i] * 3;
        }
        
        $checkDigit = (10 - ($sum % 10)) % 10;
        
        return $checkDigit === (int) $barcode[12];
    }
    
    public function message()
    {
        return 'The :attribute must be a valid UPC-A (12 digits) or EAN-13 (13 digits) barcode.';
    }
}
            

Controller Method


// app/Http/Controllers/ProductController.php
public function store(StoreProductRequest $request)
{
    // All validation has passed at this point
    $validated = $request->validated();
    
    // Start database transaction
    DB::beginTransaction();
    
    try {
        // Create the product
        $product = new Product();
        $product->name = $validated['name'];
        $product->slug = $validated['slug'];
        $product->description = $validated['description'];
        $product->short_description = $validated['short_description'] ?? null;
        $product->price = $validated['price'];
        $product->sale_price = $validated['sale_price'] ?? null;
        $product->cost = $validated['cost'] ?? null;
        $product->sku = $validated['sku'];
        $product->barcode = $validated['barcode'] ?? null;
        $product->quantity = $validated['quantity'];
        $product->backorder = $validated['backorder'];
        $product->requires_shipping = $validated['requires_shipping'];
        $product->category_id = $validated['category_id'];
        $product->has_variants = $validated['has_variants'];
        $product->weight = $validated['weight'] ?? null;
        $product->length = $validated['length'] ?? null;
        $product->width = $validated['width'] ?? null;
        $product->height = $validated['height'] ?? null;
        $product->seo_title = $validated['seo_title'] ?? null;
        $product->seo_description = $validated['seo_description'] ?? null;
        $product->seo_keywords = $validated['seo_keywords'] ?? null;
        $product->save();
        
        // Handle featured image
        if ($request->hasFile('featured_image')) {
            $path = $request->file('featured_image')->store('products', 'public');
            $product->featured_image = $path;
            $product->save();
        }
        
        // Handle gallery images
        if ($request->hasFile('gallery')) {
            foreach ($request->file('gallery') as $image) {
                $path = $image->store('products/gallery', 'public');
                $product->images()->create(['path' => $path]);
            }
        }
        
        // Handle tags
        if (isset($validated['tags'])) {
            $product->tags()->sync($validated['tags']);
        }
        
        // Handle variants
        if ($validated['has_variants'] && isset($validated['variants'])) {
            foreach ($validated['variants'] as $variantData) {
                $variant = new ProductVariant();
                $variant->product_id = $product->id;
                $variant->name = $variantData['name'];
                $variant->sku = $variantData['sku'];
                $variant->price = $variantData['price'];
                $variant->quantity = $variantData['quantity'];
                $variant->save();
            }
        }
        
        // Handle attributes
        if (isset($validated['attributes'])) {
            foreach ($validated['attributes'] as $attributeData) {
                $product->attributes()->create([
                    'name' => $attributeData['name'],
                    'value' => $attributeData['value'],
                ]);
            }
        }
        
        DB::commit();
        
        return redirect()->route('products.show', $product)
            ->with('success', 'Product created successfully!');
    } catch (\Exception $e) {
        DB::rollBack();
        
        return back()->withInput()
            ->with('error', 'An error occurred while creating the product.');
    }
}
            

This comprehensive example demonstrates:

This pattern can be adapted for many complex forms in your applications.

Validation Best Practices

Practice Activity

Custom Validation Rule Exercise

Create a custom validation rule for validating phone numbers that:

  1. Accepts common formats (e.g., +1 (555) 123-4567, 555-123-4567, 5551234567)
  2. Requires a valid country code when the international format is used
  3. Normalizes the phone number for storage (removing formatting characters)
  4. Provides helpful error messages specific to the validation failure

Conditional Validation Challenge

Create a registration form with conditional validation:

  1. Business accounts require company name, tax ID, and a business email domain
  2. Individual accounts require first name, last name, and date of birth
  3. Both account types need a valid email, password, and address
  4. If a referral code is provided, it must exist in the referral_codes table
  5. If signing up for the newsletter, a proper email is required

Advanced API Validation Exercise

Create an API endpoint for creating a new project with:

  1. Required fields: name, description, start_date
  2. Optional fields: end_date, budget, client_id, team_members
  3. Validation rules for all fields, including complex rules like:
    • end_date must be after start_date
    • budget must be a positive number with two decimal places
    • team_members must be an array of existing user IDs
  4. Custom error response format following the JSON:API specification
  5. Rate limiting to prevent abuse

Summary

In the next lecture, we'll explore advanced validation techniques, form request composition, and integrating validation with authentication and authorization systems.