Form Handling in Laravel

Module 19: PHP Backend - Laravel

Introduction to Laravel Forms

Forms are the primary way users interact with web applications, allowing them to submit information, make choices, and trigger actions. Laravel provides a robust system for creating, processing, and validating forms that makes handling user input both secure and straightforward.

Think of forms as bridges between your users and your application's data. Like any bridge, they need to be sturdy, secure, and well-designed to ensure safe passage of information in both directions.

graph TD A[User] -->|Submits| B[Form] B -->|Request| C[Controller] C -->|Validates| D[Input Data] D -->|Saves| E[Database] C -->|Response| F[View/Redirect] F -->|Feedback| A

In this lecture, we'll explore how to create forms in Laravel, process form submissions, and work with form input data effectively.

Creating Forms in Laravel

Basic Form Structure


<!-- resources/views/contact.blade.php -->
<form action="{{ route('contact.submit') }}" method="POST">
    @csrf
    
    <div class="form-group">
        <label for="name">Name</label>
        <input type="text" id="name" name="name" class="form-control" value="{{ old('name') }}">
    </div>
    
    <div class="form-group">
        <label for="email">Email</label>
        <input type="email" id="email" name="email" class="form-control" value="{{ old('email') }}">
    </div>
    
    <div class="form-group">
        <label for="message">Message</label>
        <textarea id="message" name="message" class="form-control" rows="5">{{ old('message') }}</textarea>
    </div>
    
    <button type="submit" class="btn btn-primary">Send Message</button>
</form>
            

Key elements of a Laravel form include:

Form Action and Method


<!-- Standard POST form -->
<form action="/contact" method="POST">
    @csrf
    <!-- form fields -->
</form>

<!-- Using named routes (preferred) -->
<form action="{{ route('contact.submit') }}" method="POST">
    @csrf
    <!-- form fields -->
</form>

<!-- PUT, PATCH, DELETE methods -->
<form action="{{ route('users.update', $user) }}" method="POST">
    @csrf
    @method('PUT')
    <!-- form fields -->
</form>
            

The @method directive allows you to use HTTP methods beyond GET and POST, which is important for RESTful resource controllers.

Cross-Site Request Forgery (CSRF) Protection

The @csrf directive generates a hidden input field with a token that Laravel verifies to protect against CSRF attacks:


<!-- What @csrf generates -->
<input type="hidden" name="_token" value="random_token_here">
            

This is like a security seal that ensures requests genuinely came from your forms and not from malicious sources. It's automatically verified by Laravel's middleware.

File Uploads


<form action="{{ route('profile.update') }}" method="POST" enctype="multipart/form-data">
    @csrf
    @method('PUT')
    
    <div class="form-group">
        <label for="avatar">Profile Picture</label>
        <input type="file" id="avatar" name="avatar" class="form-control-file">
    </div>
    
    <!-- other fields -->
    
    <button type="submit" class="btn btn-primary">Update Profile</button>
</form>
            

For file uploads, remember to:

Processing Form Submissions

Basic Request Handling


// routes/web.php
Route::get('/contact', [ContactController::class, 'show'])->name('contact.show');
Route::post('/contact', [ContactController::class, 'submit'])->name('contact.submit');

// app/Http/Controllers/ContactController.php
public function show()
{
    return view('contact');
}

public function submit(Request $request)
{
    // Access form data
    $name = $request->input('name');
    $email = $request->input('email');
    $message = $request->input('message');
    
    // Process the data (e.g., send an email)
    // ...
    
    // Redirect with success message
    return redirect()->route('contact.show')
        ->with('success', 'Your message has been sent!');
}
            

Accessing Form Data


public function submit(Request $request)
{
    // Get a specific input value
    $name = $request->input('name');
    
    // Get a specific input with a default value
    $name = $request->input('name', 'Guest');
    
    // Alternative syntax
    $name = $request->name;
    
    // Get only certain fields
    $credentials = $request->only(['email', 'password']);
    
    // Get all except certain fields
    $data = $request->except(['_token', '_method']);
    
    // Check if an input field exists
    if ($request->has('name')) {
        // ...
    }
    
    // Check if an input field exists and is not empty
    if ($request->filled('name')) {
        // ...
    }
    
    // Get all inputs as array
    $allInputs = $request->all();
}
            
sequenceDiagram participant User participant Form participant Controller participant Model User->>Form: Fill out and submit Form->>Controller: HTTP POST request Controller->>Controller: Access and validate input Controller->>Model: Save data Controller->>User: Redirect with success message

File Handling


public function update(Request $request)
{
    // Check if file was uploaded
    if ($request->hasFile('avatar')) {
        // Validate the file
        $request->validate([
            'avatar' => 'file|image|max:2048' // 2MB max
        ]);
        
        // Get the file instance
        $file = $request->file('avatar');
        
        // Get file properties
        $extension = $file->getClientOriginalExtension();
        $originalName = $file->getClientOriginalName();
        $size = $file->getSize();
        $mimeType = $file->getMimeType();
        
        // Store the file on disk
        $path = $file->store('avatars'); // Uses default disk
        // Or specify the disk
        $path = $file->store('avatars', 's3');
        // Or store with a custom filename
        $path = $file->storeAs('avatars', 'user_' . auth()->id() . '.' . $extension);
        
        // Update the user's avatar path
        auth()->user()->update(['avatar' => $path]);
    }
    
    return redirect()->back()->with('success', 'Profile updated!');
}
            

File uploads in Laravel use Symfony's UploadedFile class, which provides methods for validating, accessing properties, and storing files.

Request Object Deep Dive

The Illuminate\Http\Request object provides extensive functionality for working with HTTP requests:

Request Information


public function handleRequest(Request $request)
{
    // Request URI & URL
    $uri = $request->path(); // e.g., 'user/profile'
    $url = $request->url(); // e.g., 'http://example.com/user/profile'
    $fullUrl = $request->fullUrl(); // with query string
    
    // HTTP Method
    $method = $request->method();
    if ($request->isMethod('post')) {
        // Process POST request
    }
    
    // Request Headers
    $contentType = $request->header('Content-Type');
    $allHeaders = $request->headers->all();
    
    // IP Address
    $ip = $request->ip();
    
    // User Agent
    $userAgent = $request->userAgent();
    
    // Query String Parameters
    $page = $request->query('page', 1);
    $allQueryParams = $request->query();
    
    // Cookies
    $rememberToken = $request->cookie('remember_token');
}
            

Content Negotiation


public function handleRequest(Request $request)
{
    // Determine expected response type
    if ($request->expectsJson()) {
        return response()->json(['status' => 'success']);
    }
    
    // Check if request wants JSON back
    if ($request->wantsJson()) {
        return response()->json(['status' => 'success']);
    }
    
    // Accept header checks
    if ($request->accepts(['text/html', 'application/json'])) {
        // Request accepts HTML or JSON
    }
    
    // Format-specific checks
    if ($request->acceptsJson()) {
        // Request accepts JSON
    }
    
    if ($request->acceptsHtml()) {
        // Request accepts HTML
    }
    
    // Determine the best response format
    $preferredFormat = $request->prefers(['json', 'html']);
}
            

JSON Input

For API requests with JSON content, you can access the data directly:


public function apiEndpoint(Request $request)
{
    // Get JSON data
    $data = $request->json();
    
    // Get a specific value from JSON
    $name = $request->json('user.name');
    
    // You can also use input() for both form and JSON data
    $name = $request->input('user.name');
}
            

Flash Input

Laravel can temporarily store input to make it available after a redirect:


public function store(Request $request)
{
    // Validation fails
    if (!$valid) {
        // Flash input so old() helper can access it
        return redirect()->back()->withInput();
        
        // Flash only certain fields
        return redirect()->back()->withInput(
            $request->only(['name', 'email'])
        );
        
        // Flash everything except certain fields
        return redirect()->back()->withInput(
            $request->except(['password'])
        );
    }
}
            

This works with the old() helper in views to repopulate form fields:


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

Form Requests for Advanced Handling

Form Requests are dedicated request classes that encapsulate validation logic and authorization checks for a specific form:


// Generate a form request
php artisan make:request StorePostRequest

// app/Http/Requests/StorePostRequest.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; // or use Gate/Policy checks
    }

    /**
     * Get the validation rules that apply to the request.
     */
    public function rules(): array
    {
        return [
            'title' => 'required|string|max:255',
            'content' => 'required|string|min:10',
            'category_id' => 'required|exists:categories,id',
            'tags' => 'array',
            'tags.*' => 'exists:tags,id',
            'image' => 'nullable|image|max:2048',
        ];
    }
    
    /**
     * Get custom messages for validator errors.
     */
    public function messages(): array
    {
        return [
            'title.required' => 'A post title is required',
            'content.min' => 'Post content must be at least 10 characters',
            'category_id.exists' => 'The selected category is invalid',
        ];
    }
    
    /**
     * Get custom attributes for validator errors.
     */
    public function attributes(): array
    {
        return [
            'category_id' => 'category',
        ];
    }
    
    /**
     * Configure the validator instance.
     */
    public function withValidator($validator)
    {
        $validator->after(function ($validator) {
            // Perform additional validation checks
            if ($this->title === 'Admin' && !auth()->user()->isAdmin()) {
                $validator->errors()->add('title', 'You cannot use "Admin" in the title');
            }
        });
    }
    
    /**
     * Prepare the data for validation.
     */
    protected function prepareForValidation()
    {
        $this->merge([
            'slug' => \Str::slug($this->title),
        ]);
    }
}

// Using the form request in a controller
public function store(StorePostRequest $request)
{
    // Validation is automatically performed
    // Request is only executed if authorization passes
    
    $post = Post::create($request->validated());
    
    return redirect()->route('posts.show', $post)
        ->with('success', 'Post created successfully!');
}
            

Form requests provide a clean, organized way to handle form processing logic. They're like specialized security personnel for your forms - they check credentials, validate inputs, and prepare the data before it enters your application.

Working with Forms in Practice

Creating a Resource with Forms

Let's walk through a complete example of creating and processing a form to create a new blog post:

Routes


// routes/web.php
Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
            

Controller


// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;

use App\Models\Post;
use App\Models\Category;
use App\Http\Requests\StorePostRequest;
use Illuminate\Http\Request;

class PostController extends Controller
{
    /**
     * Show the form for creating a new post.
     */
    public function create()
    {
        $categories = Category::all();
        return view('posts.create', compact('categories'));
    }

    /**
     * Store a newly created post in storage.
     */
    public function store(StorePostRequest $request)
    {
        // Create the post with the validated data
        $post = Post::create($request->validated());
        
        // Handle image upload if present
        if ($request->hasFile('image')) {
            $path = $request->file('image')->store('posts', 'public');
            $post->update(['image' => $path]);
        }
        
        // Attach tags if any
        if ($request->has('tags')) {
            $post->tags()->attach($request->tags);
        }
        
        return redirect()->route('posts.show', $post)
            ->with('success', 'Post created successfully!');
    }
}
            

Form Request


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

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;

class StorePostRequest extends FormRequest
{
    public function authorize()
    {
        return auth()->check(); // User must be logged in
    }

    public function rules()
    {
        return [
            'title' => 'required|string|max:255|unique:posts',
            'content' => 'required|string|min:50',
            'category_id' => 'required|exists:categories,id',
            'tags' => 'nullable|array',
            'tags.*' => 'exists:tags,id',
            'image' => 'nullable|image|max:2048',
        ];
    }
    
    protected function prepareForValidation()
    {
        $this->merge([
            'slug' => Str::slug($this->title),
        ]);
    }
    
    public function validated($key = null, $default = null)
    {
        // Add user_id to validated data
        return array_merge(
            parent::validated(),
            ['user_id' => auth()->id()]
        );
    }
}
            

View


<!-- resources/views/posts/create.blade.php -->
@extends('layouts.app')

@section('content')
    <div class="container">
        <h1>Create New Post</h1>
        
        @if ($errors->any())
            <div class="alert alert-danger">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif
        
        <form action="{{ route('posts.store') }}" method="POST" enctype="multipart/form-data">
            @csrf
            
            <div class="form-group">
                <label for="title">Post Title</label>
                <input type="text" class="form-control @error('title') is-invalid @enderror" 
                       id="title" name="title" value="{{ old('title') }}" required>
                
                @error('title')
                    <div class="invalid-feedback">{{ $message }}</div>
                @enderror
            </div>
            
            <div class="form-group">
                <label for="category_id">Category</label>
                <select class="form-control @error('category_id') is-invalid @enderror" 
                        id="category_id" name="category_id" required>
                    <option value="">Select a category</option>
                    @foreach ($categories as $category)
                        <option value="{{ $category->id }}" 
                            {{ old('category_id') == $category->id ? 'selected' : '' }}>
                            {{ $category->name }}
                        </option>
                    @endforeach
                </select>
                
                @error('category_id')
                    <div class="invalid-feedback">{{ $message }}</div>
                @enderror
            </div>
            
            <div class="form-group">
                <label for="content">Content</label>
                <textarea class="form-control @error('content') is-invalid @enderror" 
                          id="content" name="content" rows="10" required>{{ old('content') }}</textarea>
                
                @error('content')
                    <div class="invalid-feedback">{{ $message }}</div>
                @enderror
            </div>
            
            <div class="form-group">
                <label for="image">Featured Image</label>
                <input type="file" class="form-control-file @error('image') is-invalid @enderror" 
                       id="image" name="image">
                
                @error('image')
                    <div class="invalid-feedback">{{ $message }}</div>
                @enderror
            </div>
            
            <div class="form-group">
                <label for="tags">Tags</label>
                <select class="form-control @error('tags') is-invalid @enderror" 
                        id="tags" name="tags[]" multiple>
                    @foreach ($tags as $tag)
                        <option value="{{ $tag->id }}" 
                            {{ in_array($tag->id, old('tags', [])) ? 'selected' : '' }}>
                            {{ $tag->name }}
                        </option>
                    @endforeach
                </select>
                
                @error('tags')
                    <div class="invalid-feedback">{{ $message }}</div>
                @enderror
            </div>
            
            <button type="submit" class="btn btn-primary">Create Post</button>
        </form>
    </div>
@endsection
            

This complete example demonstrates:

Form Helpers and Utilities

Error Handling in Views


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

<!-- Check for specific field errors -->
<input type="text" name="email" 
    class="form-control @error('email') is-invalid @enderror" 
    value="{{ old('email') }}">

@error('email')
    <div class="invalid-feedback">{{ $message }}</div>
@enderror

<!-- Check if a field has errors -->
@if ($errors->has('email'))
    <div class="error">{{ $errors->first('email') }}</div>
@endif
            

Session Flash Messages


<!-- In controller -->
return redirect()->route('posts.index')
    ->with('success', 'Post created successfully!');

<!-- In Blade template -->
@if (session('success'))
    <div class="alert alert-success">
        {{ session('success') }}
    </div>
@endif

@if (session('error'))
    <div class="alert alert-danger">
        {{ session('error') }}
    </div>
@endif
            

Form Components with Blade

You can create reusable form components to simplify your forms:


<!-- resources/views/components/form/input.blade.php -->
@props(['name', 'label', 'type' => 'text', 'value' => '', 'required' => false])

<div class="form-group">
    <label for="{{ $name }}">{{ $label }} @if($required)<span class="required">*</span>@endif</label>
    
    <input 
        type="{{ $type }}" 
        id="{{ $name }}" 
        name="{{ $name }}" 
        value="{{ old($name, $value) }}" 
        @if($required) required @endif
        {{ $attributes->merge(['class' => 'form-control ' . ($errors->has($name) ? 'is-invalid' : '')]) }}
    >
    
    @error($name)
        <div class="invalid-feedback">{{ $message }}</div>
    @enderror
</div>

<!-- Using the component -->
<x-form.input 
    name="email" 
    label="Email Address" 
    type="email" 
    value="{{ $user->email ?? '' }}" 
    required 
/>
            

You can create similar components for other form elements like textareas, selects, checkboxes, etc. This approach makes your forms more maintainable and consistent.

Form Macros and Custom Helpers

For more advanced form creation needs, you can create custom form macros:


// In a service provider's boot method
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Form;

public function boot()
{
    // Custom Blade directive
    Blade::directive('select2', function ($expression) {
        return "";
    });
    
    // Custom Form macro
    Form::macro('select2', function ($name, $options, $selected = null, $attributes = []) {
        $attributes['class'] = isset($attributes['class']) 
            ? $attributes['class'] . ' select2' 
            : 'select2';
            
        return Form::select($name, $options, $selected, $attributes);
    });
}

// Helper function
if (!function_exists('select2_field')) {
    function select2_field($name, $options, $selected = null, $attributes = [])
    {
        $attributes['class'] = isset($attributes['class']) 
            ? $attributes['class'] . ' select2' 
            : 'select2';
            
        return Form::select($name, $options, $selected, $attributes);
    }
}

// Usage in view
@select2('category_id', $categories, old('category_id'), ['placeholder' => 'Select Category'])
            

Custom form helpers and macros allow you to standardize complex form elements across your application, making your forms more consistent and easier to maintain.

Real-World Example: Multi-Step Form

Let's explore a more complex real-world scenario: a multi-step form for collecting user information:

graph LR A[Step 1: Personal Info] --> B[Step 2: Address] B --> C[Step 3: Account Details] C --> D[Step 4: Review & Submit]

Routes


// routes/web.php
Route::prefix('signup')->name('signup.')->group(function () {
    Route::get('/step-1', [SignupController::class, 'showStep1'])->name('step1');
    Route::post('/step-1', [SignupController::class, 'processStep1'])->name('process-step1');
    
    Route::get('/step-2', [SignupController::class, 'showStep2'])->name('step2');
    Route::post('/step-2', [SignupController::class, 'processStep2'])->name('process-step2');
    
    Route::get('/step-3', [SignupController::class, 'showStep3'])->name('step3');
    Route::post('/step-3', [SignupController::class, 'processStep3'])->name('process-step3');
    
    Route::get('/review', [SignupController::class, 'showReview'])->name('review');
    Route::post('/submit', [SignupController::class, 'submit'])->name('submit');
});
            

Controller


// app/Http/Controllers/SignupController.php
namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Requests\Signup\Step1Request;
use App\Http\Requests\Signup\Step2Request;
use App\Http\Requests\Signup\Step3Request;

class SignupController extends Controller
{
    public function showStep1()
    {
        return view('signup.step1');
    }
    
    public function processStep1(Step1Request $request)
    {
        // Store form data in session
        $request->session()->put('signup_data.personal', $request->validated());
        
        return redirect()->route('signup.step2');
    }
    
    public function showStep2(Request $request)
    {
        // Check if step 1 was completed
        if (!$request->session()->has('signup_data.personal')) {
            return redirect()->route('signup.step1');
        }
        
        return view('signup.step2');
    }
    
    public function processStep2(Step2Request $request)
    {
        // Store form data in session
        $request->session()->put('signup_data.address', $request->validated());
        
        return redirect()->route('signup.step3');
    }
    
    public function showStep3(Request $request)
    {
        // Check if previous steps were completed
        if (!$request->session()->has('signup_data.personal') || 
            !$request->session()->has('signup_data.address')) {
            return redirect()->route('signup.step1');
        }
        
        return view('signup.step3');
    }
    
    public function processStep3(Step3Request $request)
    {
        // Store form data in session
        $request->session()->put('signup_data.account', $request->validated());
        
        return redirect()->route('signup.review');
    }
    
    public function showReview(Request $request)
    {
        // Check if all steps were completed
        if (!$request->session()->has('signup_data.personal') || 
            !$request->session()->has('signup_data.address') ||
            !$request->session()->has('signup_data.account')) {
            return redirect()->route('signup.step1');
        }
        
        // Get all stored data
        $data = $request->session()->get('signup_data');
        
        return view('signup.review', compact('data'));
    }
    
    public function submit(Request $request)
    {
        // Check if all steps were completed
        if (!$request->session()->has('signup_data.personal') || 
            !$request->session()->has('signup_data.address') ||
            !$request->session()->has('signup_data.account')) {
            return redirect()->route('signup.step1');
        }
        
        // Get all stored data
        $data = $request->session()->get('signup_data');
        
        // Create user
        $user = User::create([
            'first_name' => $data['personal']['first_name'],
            'last_name' => $data['personal']['last_name'],
            'email' => $data['account']['email'],
            'password' => bcrypt($data['account']['password']),
            // Other fields...
        ]);
        
        // Create address
        $user->address()->create([
            'street' => $data['address']['street'],
            'city' => $data['address']['city'],
            'state' => $data['address']['state'],
            'zip_code' => $data['address']['zip_code'],
            // Other fields...
        ]);
        
        // Clear session data
        $request->session()->forget('signup_data');
        
        // Log in the user
        auth()->login($user);
        
        return redirect()->route('dashboard')
            ->with('success', 'Your account has been created successfully!');
    }
}
            

Form Request for Step 1


// app/Http/Requests/Signup/Step1Request.php
namespace App\Http\Requests\Signup;

use Illuminate\Foundation\Http\FormRequest;

class Step1Request extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'first_name' => 'required|string|max:100',
            'last_name' => 'required|string|max:100',
            'date_of_birth' => 'required|date|before:today',
            'phone' => 'required|string|regex:/^([0-9\s\-\+\(\)]*)$/|min:10',
        ];
    }
}
            

Step 1 View


<!-- resources/views/signup/step1.blade.php -->
@extends('layouts.signup')

@section('content')
    <div class="container">
        <div class="progress mb-4">
            <div class="progress-bar" role="progressbar" style="width: 25%;" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100">Step 1 of 4</div>
        </div>
        
        <h1>Step 1: Personal Information</h1>
        
        @if ($errors->any())
            <div class="alert alert-danger">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif
        
        <form action="{{ route('signup.process-step1') }}" method="POST">
            @csrf
            
            <div class="row">
                <div class="col-md-6">
                    <x-form.input 
                        name="first_name" 
                        label="First Name" 
                        value="{{ old('first_name', session('signup_data.personal.first_name', '')) }}" 
                        required 
                    />
                </div>
                
                <div class="col-md-6">
                    <x-form.input 
                        name="last_name" 
                        label="Last Name" 
                        value="{{ old('last_name', session('signup_data.personal.last_name', '')) }}" 
                        required 
                    />
                </div>
            </div>
            
            <div class="row">
                <div class="col-md-6">
                    <x-form.input 
                        name="date_of_birth" 
                        label="Date of Birth" 
                        type="date" 
                        value="{{ old('date_of_birth', session('signup_data.personal.date_of_birth', '')) }}" 
                        required 
                    />
                </div>
                
                <div class="col-md-6">
                    <x-form.input 
                        name="phone" 
                        label="Phone Number" 
                        type="tel" 
                        value="{{ old('phone', session('signup_data.personal.phone', '')) }}" 
                        required 
                    />
                </div>
            </div>
            
            <div class="d-flex justify-content-end mt-4">
                <button type="submit" class="btn btn-primary">Next: Address Information ></button>
            </div>
        </form>
    </div>
@endsection
            

This multi-step form example demonstrates:

This approach improves user experience by breaking down a lengthy form into digestible chunks, while still maintaining data integrity and validation.

Form Best Practices

Practice Activity

Basic Form Handling

Create a contact form with the following requirements:

  1. Fields for name, email, subject, and message
  2. Appropriate validation rules for each field
  3. Error display for validation failures
  4. Success message after successful submission
  5. Email notification to the admin with the contact message

Form Request Exercise

Create a Form Request class for a product creation form that:

  1. Validates required fields: name, price, description, category
  2. Ensures price is numeric and positive
  3. Limits description to 1000 characters
  4. Validates that an uploaded image is a valid image file under 2MB
  5. Checks that the user has permission to create products
  6. Generates a slug from the product name automatically

Advanced Form Challenge

Create a multi-part form for a job application that:

  1. Has steps for personal information, education, work experience, and references
  2. Allows adding multiple items for education and work experience
  3. Includes file uploads for resume and portfolio
  4. Saves progress between steps
  5. Provides a review page before final submission
  6. Validates each step appropriately

Summary

In the next lecture, we'll explore Laravel's validation system in more depth, covering more advanced validation scenarios and customization options.