Laravel Framework

Form Handling & Validation

18 min Lesson 10 of 45

Form Handling & Validation

Forms are the primary way users interact with web applications. Laravel provides powerful tools for handling form submissions, validating input, and displaying errors. In this lesson, you'll learn how to build secure, validated forms with elegant error handling.

Why Validation Matters

Never trust user input. Validation ensures that:

  • Data meets your requirements before processing
  • Your database stays clean and consistent
  • Your application is protected from malicious input
  • Users receive clear feedback about errors
Tip: Laravel's validation system is declarative—you describe what valid data looks like, and Laravel handles the rest.

CSRF Protection

Laravel automatically protects your application from Cross-Site Request Forgery (CSRF) attacks. Every form that uses POST, PUT, PATCH, or DELETE must include a CSRF token.

<form action="/posts" method="POST">
    @csrf
    <!-- Form fields -->
    <button type="submit">Submit</button>
</form>
Warning: Without @csrf, your POST/PUT/PATCH/DELETE requests will be rejected with a 419 error. Always include it in your forms.

Form Method Spoofing

HTML forms only support GET and POST methods. To use PUT, PATCH, or DELETE, you need to spoof the method:

<!-- Update form (PUT) -->
<form action="/posts/{{ $post->id }}" method="POST">
    @csrf
    @method('PUT')
    <!-- Form fields -->
</form>

<!-- Delete form -->
<form action="/posts/{{ $post->id }}" method="POST">
    @csrf
    @method('DELETE')
    <button type="submit">Delete</button>
</form>

Basic Validation in Controllers

The simplest way to validate is directly in your controller using the validate() method:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Post;

class PostController extends Controller
{
    public function store(Request $request)
    {
        // Validate the request
        $validated = $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
            'published_at' => 'nullable|date',
        ]);

        // If validation passes, create the post
        $post = Post::create($validated);

        return redirect()->route('posts.show', $post)
            ->with('success', 'Post created successfully!');
    }
}

If validation fails, Laravel automatically redirects back with errors and old input. No additional code needed!

Common Validation Rules

// Required & Optional
'field' => 'required'           // Must be present and not empty
'field' => 'nullable'           // Can be null
'field' => 'sometimes'          // Only validate if present

// String Rules
'field' => 'string'             // Must be string
'field' => 'max:255'            // Max 255 characters
'field' => 'min:3'              // Min 3 characters
'field' => 'between:3,255'      // Between 3-255 characters

// Numeric Rules
'field' => 'integer'            // Must be integer
'field' => 'numeric'            // Must be numeric (int/float)
'field' => 'min:1'              // Min value 1
'field' => 'max:100'            // Max value 100
'field' => 'between:1,100'      // Between 1-100

// Date Rules
'field' => 'date'               // Valid date
'field' => 'date_format:Y-m-d'  // Specific format
'field' => 'before:tomorrow'    // Before tomorrow
'field' => 'after:yesterday'    // After yesterday
'field' => 'before_or_equal:today'
'field' => 'after_or_equal:2023-01-01'

// Email & URL
'field' => 'email'              // Valid email
'field' => 'email:rfc,dns'      // Stricter email validation
'field' => 'url'                // Valid URL
'field' => 'active_url'         // URL that responds

// Boolean
'field' => 'boolean'            // true, false, 1, 0, "1", "0"
'field' => 'accepted'           // yes, on, 1, true (for T&C)

// Arrays
'field' => 'array'              // Must be array
'field' => 'array:key1,key2'    // Array with specific keys
'field.*' => 'string'           // Each array item is string

// File Upload
'field' => 'file'               // Must be file
'field' => 'image'              // Must be image
'field' => 'mimes:jpg,png,pdf'  // Specific MIME types
'field' => 'max:2048'           // Max 2MB (in kilobytes)
'field' => 'dimensions:min_width=100,min_height=100'

// Database Rules
'field' => 'unique:users,email' // Unique in users.email
'field' => 'exists:users,id'    // Exists in users.id
'field' => 'unique:users,email,' . $user->id  // Ignore current user

Multiple Rules

Combine multiple rules using pipe separator or array syntax:

// Pipe syntax
$request->validate([
    'email' => 'required|email|unique:users,email',
    'password' => 'required|min:8|confirmed',
]);

// Array syntax (recommended for complex rules)
$request->validate([
    'email' => ['required', 'email', 'unique:users,email'],
    'password' => ['required', 'min:8', 'confirmed'],
]);

Custom Validation Messages

Override default error messages:

$request->validate(
    [
        'title' => 'required|max:255',
        'email' => 'required|email|unique:users',
    ],
    [
        'title.required' => 'Please enter a title for your post.',
        'title.max' => 'The title cannot be longer than 255 characters.',
        'email.required' => 'We need your email address.',
        'email.unique' => 'This email is already registered.',
    ]
);

Custom Attribute Names

Customize attribute names in error messages:

$request->validate(
    [
        'user_email' => 'required|email',
    ],
    [],
    [
        'user_email' => 'email address',
    ]
);

// Error message: "The email address field is required."
// Instead of: "The user email field is required."

Displaying Validation Errors

Laravel automatically makes errors available to your views:

<!-- Display all errors -->
@if ($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

<!-- Display error for specific field -->
<input type="text" name="title" value="{{ old('title') }}">
@error('title')
    <div class="error">{{ $message }}</div>
@enderror

<!-- Check if field has error -->
<input type="email"
       name="email"
       class="@error('email') is-invalid @enderror"
       value="{{ old('email') }}">

Preserving Old Input

When validation fails, preserve user input with the old() helper:

<input type="text" name="title" value="{{ old('title') }}">

<textarea name="content">{{ old('content') }}</textarea>

<select name="category">
    <option value="1" {{ old('category') == 1 ? 'selected' : '' }}>Tech</option>
    <option value="2" {{ old('category') == 2 ? 'selected' : '' }}>News</option>
</select>

<input type="checkbox"
       name="terms"
       {{ old('terms') ? 'checked' : '' }}>

Form Request Validation

For complex validation logic, create a dedicated Form Request class:

# Create a form request
php artisan make:request StorePostRequest

This creates app/Http/Requests/StorePostRequest.php:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        // Return true to allow all users
        // Or add authorization logic
        return true;
    }

    /**
     * Get the validation rules.
     */
    public function rules(): array
    {
        return [
            'title' => 'required|max:255',
            'content' => 'required',
            'category_id' => 'required|exists:categories,id',
            'tags' => 'array',
            'tags.*' => 'exists:tags,id',
            'published_at' => 'nullable|date|after:now',
            'featured_image' => 'nullable|image|max:2048',
        ];
    }

    /**
     * Custom error messages.
     */
    public function messages(): array
    {
        return [
            'title.required' => 'Please provide a title for your post.',
            'category_id.exists' => 'The selected category is invalid.',
        ];
    }

    /**
     * Custom attribute names.
     */
    public function attributes(): array
    {
        return [
            'category_id' => 'category',
            'featured_image' => 'cover image',
        ];
    }
}

Using Form Request in controller:

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StorePostRequest;
use App\Models\Post;

class PostController extends Controller
{
    public function store(StorePostRequest $request)
    {
        // Validation happens automatically before this method runs
        // If validation fails, user is redirected with errors

        // Get only validated data
        $validated = $request->validated();

        $post = Post::create($validated);

        return redirect()->route('posts.show', $post)
            ->with('success', 'Post created!');
    }
}

Conditional Validation

Add rules based on conditions:

use Illuminate\Validation\Rule;

$request->validate([
    'role' => 'required|in:user,admin',

    // Only validate permissions if role is admin
    'permissions' => Rule::requiredIf(fn () => $request->role === 'admin'),

    // Only validate if field is present
    'website' => 'sometimes|url',

    // Exclude if condition is true
    'password' => 'exclude_if:oauth_login,true|required|min:8',
]);

Nested Array Validation

Validate nested arrays and objects:

// Form with multiple products
$request->validate([
    'products' => 'required|array|min:1',
    'products.*.name' => 'required|string|max:255',
    'products.*.price' => 'required|numeric|min:0',
    'products.*.quantity' => 'required|integer|min:1',
]);

// Nested objects
$request->validate([
    'user.name' => 'required|string',
    'user.email' => 'required|email',
    'user.address.street' => 'required',
    'user.address.city' => 'required',
]);

File Upload Validation

$request->validate([
    // Basic file validation
    'document' => 'required|file|max:10240',  // Max 10MB

    // Image validation
    'photo' => 'required|image|mimes:jpeg,png,jpg|max:2048',

    // Image dimensions
    'avatar' => [
        'required',
        'image',
        'dimensions:min_width=100,min_height=100,max_width=1000,max_height=1000'
    ],

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

// Handling file upload
if ($request->hasFile('photo')) {
    $path = $request->file('photo')->store('photos', 'public');
}
Exercise 1: Contact Form

Create a contact form with validation:

  • Create route, controller, and view for contact form
  • Fields: name (required, max 100), email (required, valid email), subject (required, max 200), message (required, min 10)
  • Add CSRF protection
  • Display validation errors below each field
  • Preserve old input on validation failure
  • Show success message after submission
Exercise 2: Blog Post Form Request

Create a Form Request for blog posts:

  • Create StorePostRequest and UpdatePostRequest
  • Validation: title (required, max 255), slug (required, unique), content (required, min 50), category_id (exists in categories), tags (array of existing tag IDs), featured_image (optional image, max 2MB)
  • Custom messages for all validation rules
  • Authorization: only authenticated users can create/update
  • Implement in PostController
Exercise 3: Dynamic Form Validation

Create a product form with conditional validation:

  • If product type is "physical": require weight, dimensions, shipping_cost
  • If product type is "digital": require download_link, file_size
  • All products require: name, price (min 0.01), description
  • If "is_discounted" is checked: require discount_price (less than regular price)
  • Allow multiple product images (1-5 images, each max 1MB)

Best Practices

  • Always Validate User Input: Never trust data from forms, APIs, or any external source.
  • Use Form Requests: Move complex validation logic to Form Request classes for better organization.
  • Fail Fast: Place most restrictive rules first (e.g., required before email).
  • Provide Clear Error Messages: Users should understand exactly what went wrong and how to fix it.
  • Preserve User Input: Always use old() to preserve input on validation failures.
  • Validate File Uploads: Always validate file type, size, and dimensions to prevent security issues.
  • Use CSRF Protection: Never disable CSRF protection unless you have a very good reason.

Summary

In this lesson, you've mastered:

  • CSRF protection and form method spoofing
  • Basic controller validation with common rules
  • Custom validation messages and attribute names
  • Displaying validation errors in views
  • Preserving old input with the old() helper
  • Creating Form Request classes for complex validation
  • Conditional and nested array validation
  • File upload validation and handling

Form validation is crucial for data integrity and security. With Laravel's validation system, you can build robust, user-friendly forms with minimal code!