Laravel Form Validation: Ensuring Data Integrity with Simple Rules

Published on November 18, 2025
Laravel Validation Forms Security DataIntegrity PHP

Introduction to Laravel Validation

Form validation is crucial for ensuring data integrity and application security. Laravel provides a clean, simple way to validate incoming data with powerful, readable rules.

Why Validation Matters:

  • Data Integrity: Ensure data meets application requirements
  • Security: Prevent malicious data injection
  • User Experience: Provide clear error messages
  • Database Consistency: Maintain data quality

Basic Validation Methods

1. Controller Validation (Quick and Simple)

public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users',
        'password' => 'required|min:8|confirmed',
    ]);

    // If validation passes, code continues here
    User::create($validated);
    
    return redirect()->route('users.index');
}

2. Validator Facade (More Control)

use Illuminate\Support\Facades\Validator;

public function store(Request $request)
{
    $validator = Validator::make($request->all(), [
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users',
        'password' => 'required|min:8|confirmed',
    ]);

    if ($validator->fails()) {
        return redirect('register')
                    ->withErrors($validator)
                    ->withInput();
    }

    // Store the user...
    User::create($validator->validated());
}

3. Form Request Validation (Recommended for Complex Forms)

php artisan make:request StoreUserRequest
<?php
// app/Http/Requests/StoreUserRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // Set authorization logic
    }

    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8|confirmed',
        ];
    }
}

// In controller
public function store(StoreUserRequest $request)
{
    // Validation already passed!
    User::create($request->validated());
    
    return redirect()->route('users.index');
}

Common Validation Rules

Basic Field Rules

'field' => 'required|string|max:255',
'field' => 'nullable|string',        // Optional field
'field' => 'sometimes|required|email', // Only validate if present

String Rules

'name' => 'required|string|max:255',
'title' => 'required|string|min:5|max:100',
'slug' => 'required|alpha_dash|unique:posts,slug',
'description' => 'nullable|string|min:10|max:1000',
'bio' => 'string|between:10,5000',   // Between min and max

Numeric Rules

'age' => 'required|integer|min:18|max:100',
'price' => 'required|numeric|min:0|max:10000',
'quantity' => 'integer|min:1',
'rating' => 'numeric|between:1,5',   // Between 1 and 5
'discount' => 'nullable|numeric|min:0|max:100',

Email and URL Rules

'email' => 'required|email|unique:users,email',
'website' => 'nullable|url|max:255',
'personal_url' => 'sometimes|url|active_url', // Must be active URL
'email_array' => 'required|array',
'email_array.*' => 'email', // Validate each array element

Date Rules

'birth_date' => 'required|date|before:today',
'start_date' => 'required|date|after:today',
'end_date' => 'required|date|after:start_date',
'published_at' => 'nullable|date|after_or_equal:today',
'event_date' => 'date_format:Y-m-d H:i:s', // Specific format

File Upload Rules

'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048', // 2MB
'document' => 'nullable|file|mimes:pdf,doc,docx|max:5120', // 5MB
'photos' => 'sometimes|array|max:5',
'photos.*' => 'image|mimes:jpeg,png,jpg|max:1024', // Each photo
'resume' => 'required|mimetypes:application/pdf,application/msword',

Array and Collection Rules

'tags' => 'required|array|min:1|max:10',
'tags.*' => 'string|distinct|min:2|max:50', // No duplicates
'sizes' => 'array',
'sizes.*' => 'in:small,medium,large,xl', // Must be in list
'preferences' => 'sometimes|array',
'preferences.color' => 'required_if:preferences,array|string',

Database Rules

'email' => 'required|email|unique:users,email,NULL,id,account_id,1',
'category_id' => 'required|exists:categories,id',
'user_id' => 'exists:users,id,status,active', // User must exist and be active
'slug' => 'unique:posts,slug,' . $postId, // Ignore current post

Real-World Validation Examples

Example 1: User Registration Form

public function rules(): array
{
    return [
        'first_name' => 'required|string|max:50|alpha',
        'last_name' => 'required|string|max:50|alpha',
        'username' => 'required|string|alpha_dash|min:3|max:30|unique:users',
        'email' => 'required|email:rfc,dns|max:255|unique:users',
        'password' => [
            'required',
            'string',
            'min:8',
            'confirmed',
            'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/', // Strong password
        ],
        'phone' => 'nullable|string|regex:/^\+?[1-9]\d{1,14}$/', // E.164 format
        'birth_date' => 'required|date|before:-18 years', // Must be 18+ years old
        'agree_terms' => 'required|accepted', // Checkbox must be checked
        'newsletter' => 'sometimes|boolean',
    ];
}

Example 2: E-commerce Product Creation

public function rules(): array
{
    return [
        'name' => 'required|string|max:255|unique:products,name',
        'slug' => 'required|string|alpha_dash|max:255|unique:products,slug',
        'description' => 'required|string|min:50|max:2000',
        'short_description' => 'required|string|max:300',
        'price' => 'required|numeric|min:0|max:999999.99',
        'sale_price' => 'nullable|numeric|lt:price', // Must be less than regular price
        'sku' => 'required|string|max:100|unique:products,sku',
        'stock_quantity' => 'required|integer|min:0|max:10000',
        'weight' => 'nullable|numeric|min:0',
        'dimensions' => 'nullable|array',
        'dimensions.length' => 'required_with:dimensions|numeric|min:0',
        'dimensions.width' => 'required_with:dimensions|numeric|min:0',
        'dimensions.height' => 'required_with:dimensions|numeric|min:0',
        'category_id' => 'required|exists:categories,id',
        'brand_id' => 'nullable|exists:brands,id',
        'tags' => 'sometimes|array|max:10',
        'tags.*' => 'string|max:50',
        'images' => 'sometimes|array|max:5',
        'images.*' => 'image|mimes:jpeg,png,jpg,webp|max:5120',
        'is_featured' => 'sometimes|boolean',
        'is_active' => 'sometimes|boolean',
        'meta_title' => 'nullable|string|max:60',
        'meta_description' => 'nullable|string|max:160',
    ];
}

Example 3: Blog Post with Polymorphic Relationships

public function rules(): array
{
    return [
        'title' => 'required|string|min:10|max:255',
        'slug' => 'required|string|alpha_dash|max:255|unique:posts,slug,' . $this->post?->id,
        'content' => 'required|string|min:100|max:10000',
        'excerpt' => 'required|string|max:500',
        'featured_image' => 'sometimes|image|mimes:jpeg,png,jpg,webp|max:2048',
        'category_id' => 'required|exists:categories,id',
        'tags' => 'required|array|min:1|max:5',
        'tags.*' => 'exists:tags,id',
        'is_published' => 'sometimes|boolean',
        'published_at' => 'required_if:is_published,true|nullable|date|after_or_equal:now',
        'meta_title' => 'nullable|string|max:60',
        'meta_description' => 'nullable|string|max:160',
        'reading_time' => 'sometimes|integer|min:1|max:60',
    ];
}

Custom Validation Rules

Creating Custom Rules

php artisan make:rule StrongPassword
<?php
// app/Rules/StrongPassword.php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class StrongPassword implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // Check minimum length
        if (strlen($value) < 8) {
            $fail('The :attribute must be at least 8 characters.');
            return;
        }

        // Check for at least one uppercase letter
        if (!preg_match('/[A-Z]/', $value)) {
            $fail('The :attribute must contain at least one uppercase letter.');
            return;
        }

        // Check for at least one lowercase letter
        if (!preg_match('/[a-z]/', $value)) {
            $fail('The :attribute must contain at least one lowercase letter.');
            return;
        }

        // Check for at least one number
        if (!preg_match('/[0-9]/', $value)) {
            $fail('The :attribute must contain at least one number.');
            return;
        }

        // Check for at least one special character
        if (!preg_match('/[@$!%*#?&]/', $value)) {
            $fail('The :attribute must contain at least one special character (@$!%*#?&).');
        }
    }
}

// Usage in form request
public function rules(): array
{
    return [
        'password' => ['required', 'confirmed', new StrongPassword],
    ];
}

Custom Rule with Parameters

<?php
// app/Rules/FileType.php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class FileType implements ValidationRule
{
    protected $types;

    public function __construct(array $types)
    {
        $this->types = $types;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $extension = strtolower($value->getClientOriginalExtension());
        
        if (!in_array($extension, $this->types)) {
            $fail('The :attribute must be a file of type: ' . implode(', ', $this->types));
        }
    }
}

// Usage
public function rules(): array
{
    return [
        'document' => ['required', 'file', new FileType(['pdf', 'doc', 'docx'])],
    ];
}

Inline Custom Validation

use Illuminate\Support\Facades\Validator;

$validator = Validator::make($request->all(), [
    'start_date' => 'required|date',
    'end_date' => 'required|date',
]);

$validator->after(function ($validator) use ($request) {
    if ($request->start_date > $request->end_date) {
        $validator->errors()->add(
            'end_date', 
            'End date must be after start date.'
        );
    }
    
    if (Carbon::parse($request->start_date)->diffInDays($request->end_date) > 30) {
        $validator->errors()->add(
            'end_date',
            'The booking period cannot exceed 30 days.'
        );
    }
});

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

Custom Validation Messages

Custom Error Messages

public function messages(): array
{
    return [
        'name.required' => 'The full name field is required.',
        'email.required' => 'We need your email address to create your account.',
        'email.unique' => 'This email address is already registered. Please try logging in.',
        'password.required' => 'Please choose a secure password for your account.',
        'password.min' => 'Your password should be at least 8 characters long.',
        'password.confirmed' => 'The password confirmation does not match.',
        'agree_terms.accepted' => 'You must accept the terms and conditions to continue.',
        'birth_date.before' => 'You must be at least 18 years old to register.',
        
        // Array field messages
        'tags.*.string' => 'Each tag must be a text value.',
        'tags.*.distinct' => 'Duplicate tags are not allowed.',
        'images.*.image' => 'Each file must be a valid image.',
        'images.*.max' => 'Each image must not be larger than 5MB.',
    ];
}

Custom Attribute Names

public function attributes(): array
{
    return [
        'first_name' => 'first name',
        'last_name' => 'last name',
        'email' => 'email address',
        'agree_terms' => 'terms and conditions',
        'birth_date' => 'date of birth',
        'category_id' => 'category',
        'tags.*' => 'tag',
        'images.*' => 'image',
    ];
}

Form Request Validation with Authorization

Complete Form Request Example

<?php
// app/Http/Requests/UpdateProfileRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UpdateProfileRequest extends FormRequest
{
    public function authorize(): bool
    {
        // Only allow users to update their own profile
        return auth()->check() && auth()->id() == $this->user->id;
    }

    public function rules(): array
    {
        $userId = auth()->id();

        return [
            'name' => 'required|string|max:255',
            'email' => [
                'required',
                'email',
                Rule::unique('users')->ignore($userId),
            ],
            'username' => [
                'required',
                'alpha_dash',
                'min:3',
                'max:30',
                Rule::unique('users')->ignore($userId),
            ],
            'phone' => 'nullable|string|regex:/^\+?[1-9]\d{1,14}$/',
            'bio' => 'nullable|string|max:1000',
            'website' => 'nullable|url|max:255',
            'location' => 'nullable|string|max:100',
            'avatar' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
            'social_links' => 'sometimes|array',
            'social_links.facebook' => 'nullable|url|starts_with:https://facebook.com,https://www.facebook.com',
            'social_links.twitter' => 'nullable|url|starts_with:https://twitter.com,https://x.com',
            'social_links.linkedin' => 'nullable|url|starts_with:https://linkedin.com,https://www.linkedin.com',
            'notification_settings' => 'sometimes|array',
            'notification_settings.email' => 'boolean',
            'notification_settings.push' => 'boolean',
            'notification_settings.sms' => 'boolean',
        ];
    }

    public function messages(): array
    {
        return [
            'email.unique' => 'This email address is already taken. Please use a different one.',
            'username.unique' => 'This username is not available. Please choose another one.',
            'phone.regex' => 'Please enter a valid phone number in international format.',
            'avatar.max' => 'The profile picture must not be larger than 2MB.',
            'social_links.facebook.starts_with' => 'Please enter a valid Facebook profile URL.',
            'social_links.twitter.starts_with' => 'Please enter a valid Twitter profile URL.',
            'social_links.linkedin.starts_with' => 'Please enter a valid LinkedIn profile URL.',
        ];
    }

    public function attributes(): array
    {
        return [
            'social_links.facebook' => 'Facebook URL',
            'social_links.twitter' => 'Twitter URL',
            'social_links.linkedin' => 'LinkedIn URL',
            'notification_settings.email' => 'email notifications',
            'notification_settings.push' => 'push notifications',
            'notification_settings.sms' => 'SMS notifications',
        ];
    }

    public function withValidator($validator)
    {
        $validator->after(function ($validator) {
            if ($this->hasFile('avatar') && !$this->file('avatar')->isValid()) {
                $validator->errors()->add('avatar', 'The avatar file upload failed. Please try again.');
            }
        });
    }
}

Conditional Validation

sometimes() and required_if Rules

public function rules(): array
{
    return [
        'payment_method' => 'required|in:credit_card,paypal,bank_transfer',
        
        // Credit card validation (only if payment_method is credit_card)
        'card_number' => 'required_if:payment_method,credit_card|string|size:16',
        'expiry_date' => 'required_if:payment_method,credit_card|date_format:m/y|after:today',
        'cvv' => 'required_if:payment_method,credit_card|string|size:3',
        'card_holder' => 'required_if:payment_method,credit_card|string|max:255',
        
        // PayPal validation
        'paypal_email' => 'required_if:payment_method,paypal|email',
        
        // Bank transfer validation
        'account_holder' => 'required_if:payment_method,bank_transfer|string|max:255',
        'account_number' => 'required_if:payment_method,bank_transfer|string|size:8',
        'sort_code' => 'required_if:payment_method,bank_transfer|string|size:6',
        
        // Billing address (always required)
        'billing_address' => 'required|array',
        'billing_address.street' => 'required|string|max:255',
        'billing_address.city' => 'required|string|max:100',
        'billing_address.state' => 'required|string|max:100',
        'billing_address.zip_code' => 'required|string|max:20',
        'billing_address.country' => 'required|string|max:100',
        
        // Shipping address (only required if different from billing)
        'shipping_same_as_billing' => 'sometimes|boolean',
        'shipping_address' => 'required_unless:shipping_same_as_billing,true|array',
        'shipping_address.street' => 'required_with:shipping_address|string|max:255',
        'shipping_address.city' => 'required_with:shipping_address|string|max:100',
        'shipping_address.state' => 'required_with:shipping_address|string|max:100',
        'shipping_address.zip_code' => 'required_with:shipping_address|string|max:20',
        'shipping_address.country' => 'required_with:shipping_address|string|max:100',
    ];
}

Dynamic Validation Rules

public function rules(): array
{
    $rules = [
        'type' => 'required|in:individual,company',
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:customers,email',
    ];

    // Add conditional rules based on customer type
    if ($this->type === 'individual') {
        $rules['date_of_birth'] = 'required|date|before:-18 years';
        $rules['personal_id'] = 'required|string|max:50';
    } elseif ($this->type === 'company') {
        $rules['company_name'] = 'required|string|max:255';
        $rules['tax_id'] = 'required|string|max:50';
        $rules['registration_number'] = 'required|string|max:100';
    }

    // Add phone validation if phone is provided
    if ($this->has('phone')) {
        $rules['phone'] = 'string|regex:/^\+?[1-9]\d{1,14}$/';
    }

    return $rules;
}

Validation in Blade Templates

Displaying Validation Errors

<form method="POST" action="{{ route('users.store') }}">
    @csrf
    
    <!-- Name Field -->
    <div class="form-group">
        <label for="name">Full Name</label>
        <input 
            type="text" 
            name="name" 
            id="name" 
            value="{{ old('name') }}"
            class="form-control @error('name') is-invalid @enderror"
            required
        >
        @error('name')
            <div class="invalid-feedback">
                {{ $message }}
            </div>
        @enderror
    </div>
    
    <!-- Email Field -->
    <div class="form-group">
        <label for="email">Email Address</label>
        <input 
            type="email" 
            name="email" 
            id="email" 
            value="{{ old('email') }}"
            class="form-control @error('email') is-invalid @enderror"
            required
        >
        @error('email')
            <div class="invalid-feedback">
                {{ $message }}
            </div>
        @enderror
    </div>
    
    <!-- Password Field -->
    <div class="form-group">
        <label for="password">Password</label>
        <input 
            type="password" 
            name="password" 
            id="password"
            class="form-control @error('password') is-invalid @enderror"
            required
        >
        @error('password')
            <div class="invalid-feedback">
                {{ $message }}
            </div>
        @enderror
    </div>
    
    <!-- Password Confirmation -->
    <div class="form-group">
        <label for="password_confirmation">Confirm Password</label>
        <input 
            type="password" 
            name="password_confirmation" 
            id="password_confirmation"
            class="form-control"
            required
        >
    </div>
    
    <!-- Array Fields (Tags) -->
    <div class="form-group">
        <label for="tags">Tags</label>
        @for($i = 0; $i < 3; $i++)
            <input 
                type="text" 
                name="tags[]" 
                value="{{ old('tags.' . $i) }}"
                class="form-control mb-2 @error('tags.' . $i) is-invalid @enderror"
                placeholder="Tag {{ $i + 1 }}"
            >
            @error('tags.' . $i)
                <div class="invalid-feedback d-block">
                    {{ $message }}
                </div>
            @enderror
        @endfor
        @error('tags')
            <div class="invalid-feedback d-block">
                {{ $message }}
            </div>
        @enderror
    </div>
    
    <button type="submit" class="btn btn-primary">Register</button>
</form>

<!-- Display all errors at the top -->
@if($errors->any())
    <div class="alert alert-danger">
        <h5>Please fix the following errors:</h5>
        <ul class="mb-0">
            @foreach($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

API Validation Responses

JSON Validation Responses

public function store(StoreUserRequest $request)
{
    // For API, Laravel automatically returns JSON validation errors
    $user = User::create($request->validated());
    
    return response()->json([
        'message' => 'User created successfully',
        'user' => $user
    ], 201);
}

// Custom API validation response
public function failedValidation(Validator $validator)
{
    throw new HttpResponseException(response()->json([
        'success' => false,
        'message' => 'Validation errors',
        'errors' => $validator->errors()
    ], 422));
}

Custom API Form Request

<?php
// app/Http/Requests/ApiRequest.php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

abstract class ApiRequest extends FormRequest
{
    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(response()->json([
            'success' => false,
            'message' => 'Validation failed',
            'errors' => $validator->errors()
        ], 422));
    }

    protected function failedAuthorization()
    {
        throw new HttpResponseException(response()->json([
            'success' => false,
            'message' => 'This action is unauthorized.'
        ], 403));
    }
}

// Use in specific API requests
class StoreUserRequest extends ApiRequest
{
    // Your rules here...
}

Testing Validation

Validation Test Examples

<?php
// tests/Feature/UserRegistrationTest.php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserRegistrationTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_registration_validation()
    {
        // Test required fields
        $response = $this->post('/register', []);
        
        $response->assertSessionHasErrors([
            'name', 'email', 'password'
        ]);

        // Test email validation
        $response = $this->post('/register', [
            'name' => 'John Doe',
            'email' => 'invalid-email',
            'password' => 'password',
            'password_confirmation' => 'password',
        ]);

        $response->assertSessionHasErrors('email');

        // Test password confirmation
        $response = $this->post('/register', [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password',
            'password_confirmation' => 'different',
        ]);

        $response->assertSessionHasErrors('password');

        // Test unique email
        $user = \App\Models\User::factory()->create(['email' => 'existing@example.com']);
        
        $response = $this->post('/register', [
            'name' => 'Jane Doe',
            'email' => 'existing@example.com',
            'password' => 'password123',
            'password_confirmation' => 'password123',
        ]);

        $response->assertSessionHasErrors('email');
    }

    public function test_successful_user_registration()
    {
        $response = $this->post('/register', [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'Password123!',
            'password_confirmation' => 'Password123!',
            'agree_terms' => true,
        ]);

        $response->assertRedirect('/dashboard');
        $this->assertDatabaseHas('users', [
            'email' => 'john@example.com'
        ]);
    }
}

Common Interview Questions & Answers

1. What's the difference between validate() and Validator::make()?

$request->validate() is a convenient helper that automatically redirects back with errors, while Validator::make() gives you more control and returns a validator instance that you can check manually.

2. How do you create custom validation rules?

Use php artisan make:rule RuleName to generate a custom rule class, then implement the validate method with your custom logic. You can also use closure-based rules with Validator::make().

3. What's the purpose of Form Request classes?

Form Request classes centralize validation logic, handle authorization, and can customize error messages and responses. They keep controllers clean and make validation reusable.

4. How do you handle conditional validation?

Use required_if, required_unless, required_with, or the sometimes rule. You can also use conditional logic in form requests to dynamically build rules arrays.

5. What's the difference between unique and exists rules?

unique checks that a value doesn't already exist in the database, while exists checks that a value does exist in the database. Both are used for database integrity.

6. How do you validate array fields?

Use dot notation: 'tags.*' => 'required|string' validates each element in the tags array. You can also nest deeper: 'users.*.email' => 'required|email'.

Best Practices

  • Use Form Requests for complex validation scenarios
  • Provide clear, user-friendly error messages
  • Validate early in the request lifecycle
  • Use specific rules rather than generic ones when possible
  • Test your validation rules thoroughly
  • Keep validation logic close to the data it validates
  • Use database constraints in addition to form validation
  • Sanitize data where appropriate, but don't rely solely on validation for security

Validation Rule Cheat Sheet

// Basic
'required', 'nullable', 'sometimes'

// Strings
'string', 'email', 'url', 'ip', 'alpha', 'alpha_dash', 'alpha_num'

// Numbers  
'integer', 'numeric', 'min:value', 'max:value', 'between:min,max'

// Dates
'date', 'date_format:format', 'before:date', 'after:date'

// Files
'file', 'image', 'mimes:jpeg,png', 'mimetypes:image/jpeg', 'max:size'

// Database
'exists:table,column', 'unique:table,column,except,idColumn'

// Arrays
'array', 'size:count', 'min:count', 'max:count'

// Conditions
'required_if:field,value', 'required_unless:field,value', 'required_with:field'

You've now mastered Laravel form validation and can ensure data integrity across your applications! In our next post, we'll explore Laravel Authentication Scaffolding: A Deep Dive into Laravel Breeze & Jetstream to learn about Laravel's authentication systems.