Form Request Composition and Advanced Techniques

Module 19: PHP Backend - Laravel

Introduction to Form Request Composition

In previous lectures, we explored the basics of form handling and validation rules. In this lecture, we'll dive deeper into more advanced techniques, focusing on Form Request classes, how to compose them, and how to leverage them for complex validation scenarios.

Form Request classes are a powerful feature of Laravel that allows you to encapsulate validation logic, authorization checks, and input preprocessing in dedicated classes. Think of Form Requests as specialized gatekeepers for your application - each one is designed to guard a specific entry point with custom rules and policies.

flowchart TD A[HTTP Request] --> B[Form Request] B -->|Authorization Fails| C[403 Forbidden] B -->|Validation Fails| D[Redirect with Errors] B -->|Success| E[Controller Method] E --> F[Business Logic] F --> G[Response]

By mastering advanced Form Request techniques, you can create more maintainable, testable, and robust applications.

Form Request Architecture

Let's take a closer look at the anatomy of a Form Request class:


namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest 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 [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8|confirmed',
        ];
    }
    
    /**
     * Get custom messages for validator errors.
     */
    public function messages(): array
    {
        return [
            'email.unique' => 'This email address is already in use.',
        ];
    }
    
    /**
     * Get custom attributes for validator errors.
     */
    public function attributes(): array
    {
        return [
            'email' => 'email address',
        ];
    }
    
    /**
     * Prepare the data for validation.
     */
    protected function prepareForValidation(): void
    {
        $this->merge([
            'name' => trim($this->name),
            'email' => strtolower($this->email),
        ]);
    }
    
    /**
     * Handle a passed validation attempt.
     */
    protected function passedValidation(): void
    {
        $this->replace([
            // Modify the validated data
            'password' => bcrypt($this->password),
        ]);
    }
    
    /**
     * Get the validator instance for the request.
     */
    protected function getValidatorInstance()
    {
        $validator = parent::getValidatorInstance();
        
        $validator->after(function ($validator) {
            // Perform additional validation that depends on multiple fields
            if ($this->input('is_admin') && !$this->user()->isAllowedToCreateAdmins()) {
                $validator->errors()->add('is_admin', 'You cannot create admin users.');
            }
        });
        
        return $validator;
    }
}
            

Form Requests extend the base FormRequest class and provide a variety of hooks for customizing the validation process. Think of these hooks as lifecycle events - each one gives you an opportunity to modify the request at a specific stage of processing.

graph TD A[HTTP Request] --> B[prepareForValidation] B --> C[getValidatorInstance] C --> D[rules/messages/attributes] D --> E[authorize] E -->|Authorized| F[Validation] E -->|Not Authorized| G[403 Forbidden] F -->|Passes| H[passedValidation] F -->|Fails| I[Redirect with Errors] H --> J[Controller Method]

The form request lifecycle provides multiple points for intervening in the request processing flow.

Authorization in Form Requests

The authorize() method in Form Requests is a powerful way to combine validation with authorization:

Basic Authorization


/**
 * Determine if the user is authorized to make this request.
 */
public function authorize(): bool
{
    // Simple check - only authenticated users can access
    return auth()->check();
}
            

Using Gates


/**
 * Determine if the user is authorized to make this request.
 */
public function authorize(): bool
{
    // Using Laravel's Gate facade
    return Gate::allows('create-post');
}
            

Using Policies


/**
 * Determine if the user is authorized to make this request.
 */
public function authorize(): bool
{
    // For creating a new post
    return $this->user()->can('create', Post::class);
    
    // For updating an existing post
    $post = Post::findOrFail($this->route('post'));
    return $this->user()->can('update', $post);
}
            

Complex Authorization Logic


/**
 * Determine if the user is authorized to make this request.
 */
public function authorize(): bool
{
    // Get the post from the route parameter
    $post = Post::findOrFail($this->route('post'));
    
    // Multiple conditions for authorization
    return $this->user()->isAdmin() || 
           $this->user()->id === $post->user_id || 
           $this->user()->hasRole('editor');
}
            

The authorize() method acts as a gatekeeper before validation even begins. If it returns false, the request is immediately terminated with a 403 Forbidden response. This ensures that unauthorized users can't even attempt to validate or process form submissions.

Think of authorization as the first line of defense - it checks if the user has the right security clearance before even examining the details of their request.

Advanced Validation Techniques

Dynamic Rules Based on User


/**
 * Get the validation rules that apply to the request.
 */
public function rules(): array
{
    $rules = [
        'title' => 'required|string|max:255',
        'content' => 'required|string|min:10',
    ];
    
    // Add stricter rules for regular users
    if (!$this->user()->isAdmin()) {
        $rules['content'] = 'required|string|min:50|profanity_free';
        $rules['category_id'] = 'required|exists:categories,id,is_restricted,0';
    }
    
    return $rules;
}
            

Rules Based on Request Method


/**
 * Get the validation rules that apply to the request.
 */
public function rules(): array
{
    $rules = [
        'name' => 'required|string|max:255',
        'email' => 'required|email|max:255|unique:users',
    ];
    
    // Add password rules only for new users (POST requests)
    if ($this->isMethod('POST')) {
        $rules['password'] = 'required|min:8|confirmed';
    }
    
    // For updates (PUT/PATCH), make email unique except for the current user
    if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
        $userId = $this->route('user');
        $rules['email'] = "required|email|max:255|unique:users,email,{$userId}";
        
        // Password is optional on update, but must still meet requirements if provided
        $rules['password'] = 'nullable|min:8|confirmed';
    }
    
    return $rules;
}
            

Rules Based on Input Values


/**
 * Get the validation rules that apply to the request.
 */
public function rules(): array
{
    $rules = [
        'payment_type' => 'required|in:credit_card,paypal,bank_transfer',
    ];
    
    // Add rules based on selected payment type
    switch ($this->input('payment_type')) {
        case 'credit_card':
            $rules['card_number'] = 'required|string|credit_card';
            $rules['expiration'] = 'required|string|date_format:m/y';
            $rules['cvv'] = 'required|numeric|digits:3,4';
            break;
            
        case 'paypal':
            $rules['paypal_email'] = 'required|email';
            break;
            
        case 'bank_transfer':
            $rules['bank_name'] = 'required|string';
            $rules['account_number'] = 'required|string';
            $rules['routing_number'] = 'required|string';
            break;
    }
    
    return $rules;
}
            

Advanced Preprocessing


/**
 * Prepare the data for validation.
 */
protected function prepareForValidation(): void
{
    // Only modify the data if it's present
    if ($this->has('email')) {
        $this->merge([
            'email' => strtolower(trim($this->email)),
        ]);
    }
    
    // Generate a slug from the title
    if ($this->has('title') && !$this->has('slug')) {
        $this->merge([
            'slug' => \Str::slug($this->title),
        ]);
    }
    
    // Parse and format a date
    if ($this->has('birth_date') && $this->birth_date) {
        try {
            $date = Carbon::createFromFormat('m/d/Y', $this->birth_date);
            $this->merge([
                'birth_date' => $date->format('Y-m-d'),
            ]);
        } catch (\Exception $e) {
            // Let validation handle the invalid date
        }
    }
    
    // Handle comma-separated tags
    if ($this->has('tags') && is_string($this->tags)) {
        $this->merge([
            'tags' => array_map('trim', explode(',', $this->tags)),
        ]);
    }
}
            

These techniques allow you to adapt your validation rules based on the context of the request, making your form handling more dynamic and flexible. It's like having a security system that adjusts its protocols based on who's approaching, what they're carrying, and what door they're trying to enter.

Composing Form Requests

As applications grow, you might find yourself repeating validation logic across multiple form requests. There are several approaches to reuse validation code:

Using Traits


// app/Http/Requests/Traits/HasUserValidation.php
namespace App\Http\Requests\Traits;

trait HasUserValidation
{
    /**
     * Get user validation rules.
     *
     * @return array
     */
    protected function getUserRules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|max:255|unique:users,email' . 
                      ($this->user ? ',' . $this->user->id : ''),
            'password' => $this->isMethod('POST') ? 'required|min:8|confirmed' 
                                                  : 'nullable|min:8|confirmed',
        ];
    }
    
    /**
     * Get user validation messages.
     *
     * @return array
     */
    protected function getUserMessages(): array
    {
        return [
            'email.unique' => 'This email address is already in use.',
            'password.min' => 'Your password must be at least 8 characters long.',
        ];
    }
}

// Using the trait in a Form Request
namespace App\Http\Requests;

use App\Http\Requests\Traits\HasUserValidation;
use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
    use HasUserValidation;
    
    public function rules(): array
    {
        return array_merge($this->getUserRules(), [
            'role' => 'required|in:admin,editor,user',
            'department_id' => 'required|exists:departments,id',
        ]);
    }
    
    public function messages(): array
    {
        return array_merge($this->getUserMessages(), [
            'role.required' => 'Please select a user role.',
            'department_id.required' => 'Please select a department.',
        ]);
    }
}
            

Using Rule Objects for Reusability


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

use Illuminate\Contracts\Validation\Rule;

class StrongPassword implements Rule
{
    // Implementation from previous lecture
}

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

use Illuminate\Contracts\Validation\Rule;

class PhoneNumber implements Rule
{
    // Implementation for phone validation
}

// Using shared rules in multiple form requests
namespace App\Http\Requests;

use App\Rules\StrongPassword;
use App\Rules\PhoneNumber;
use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'password' => ['required', new StrongPassword],
            'phone' => ['required', new PhoneNumber],
            // Other rules...
        ];
    }
}
            

Base Form Request Classes


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

use Illuminate\Foundation\Http\FormRequest;

abstract class BaseUserRequest extends FormRequest
{
    /**
     * Get the base validation rules for users.
     *
     * @return array
     */
    protected function baseRules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|max:255',
            'phone' => 'nullable|string|regex:/^([0-9\s\-\+\(\)]*)$/|min:10',
        ];
    }
    
    /**
     * Prepare the data for validation.
     */
    protected function prepareForValidation(): void
    {
        $this->merge([
            'name' => trim($this->name),
            'email' => strtolower(trim($this->email)),
        ]);
    }
}

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

class StoreUserRequest extends BaseUserRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', User::class);
    }

    public function rules(): array
    {
        return array_merge($this->baseRules(), [
            'email' => 'required|email|max:255|unique:users',
            'password' => 'required|min:8|confirmed',
            'role_id' => 'required|exists:roles,id',
        ]);
    }
}

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

class UpdateUserRequest extends BaseUserRequest
{
    public function authorize(): bool
    {
        $user = User::findOrFail($this->route('user'));
        return $this->user()->can('update', $user);
    }

    public function rules(): array
    {
        $userId = $this->route('user');
        
        return array_merge($this->baseRules(), [
            'email' => "required|email|max:255|unique:users,email,{$userId}",
            'password' => 'nullable|min:8|confirmed',
            'role_id' => 'sometimes|required|exists:roles,id',
        ]);
    }
}
            

These composition techniques allow you to create a modular, maintainable structure for your form requests. It's like building with standardized components - you can assemble complex structures more efficiently by reusing well-designed parts.

Testing Form Requests

Testing is a crucial aspect of form request development. Laravel provides tools to make testing form requests straightforward:

Testing a Form Request in Isolation


namespace Tests\Unit\Requests;

use App\Http\Requests\StoreUserRequest;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;

class StoreUserRequestTest extends TestCase
{
    use RefreshDatabase, WithFaker;
    
    /** @var \App\Http\Requests\StoreUserRequest */
    private $rules;
    
    protected function setUp(): void
    {
        parent::setUp();
        
        $this->rules = (new StoreUserRequest())->rules();
    }
    
    /** @test */
    public function name_is_required()
    {
        $validator = Validator::make(
            ['name' => ''],
            $this->rules
        );
        
        $this->assertTrue($validator->fails());
        $this->assertTrue($validator->errors()->has('name'));
    }
    
    /** @test */
    public function email_must_be_valid()
    {
        $validator = Validator::make(
            ['email' => 'not-valid-email'],
            $this->rules
        );
        
        $this->assertTrue($validator->fails());
        $this->assertTrue($validator->errors()->has('email'));
    }
    
    /** @test */
    public function email_must_be_unique()
    {
        // Create a user with the email
        factory(\App\Models\User::class)->create([
            'email' => 'test@example.com',
        ]);
        
        $validator = Validator::make(
            ['email' => 'test@example.com'],
            $this->rules
        );
        
        $this->assertTrue($validator->fails());
        $this->assertTrue($validator->errors()->has('email'));
    }
    
    /** @test */
    public function valid_data_passes_validation()
    {
        $validator = Validator::make(
            [
                'name' => 'John Doe',
                'email' => 'unique-' . $this->faker->safeEmail,
                'password' => 'password123',
                'password_confirmation' => 'password123',
                'role_id' => factory(\App\Models\Role::class)->create()->id,
            ],
            $this->rules
        );
        
        $this->assertFalse($validator->fails());
    }
}
            

Testing Authorization Logic


namespace Tests\Unit\Requests;

use App\Http\Requests\UpdatePostRequest;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UpdatePostRequestTest extends TestCase
{
    use RefreshDatabase;
    
    /** @test */
    public function owners_can_update_their_posts()
    {
        $user = factory(User::class)->create();
        $post = factory(Post::class)->create(['user_id' => $user->id]);
        
        $request = UpdatePostRequest::create(
            route('posts.update', $post),
            'PUT'
        );
        
        $request->setRouteResolver(function () use ($post) {
            return app('router')->getRoutes()->match(
                request()->create(route('posts.update', $post), 'PUT')
            );
        });
        
        $request->setUserResolver(function () use ($user) {
            return $user;
        });
        
        $this->assertTrue($request->authorize());
    }
    
    /** @test */
    public function non_owners_cannot_update_posts()
    {
        $owner = factory(User::class)->create();
        $post = factory(Post::class)->create(['user_id' => $owner->id]);
        
        $otherUser = factory(User::class)->create();
        
        $request = UpdatePostRequest::create(
            route('posts.update', $post),
            'PUT'
        );
        
        $request->setRouteResolver(function () use ($post) {
            return app('router')->getRoutes()->match(
                request()->create(route('posts.update', $post), 'PUT')
            );
        });
        
        $request->setUserResolver(function () use ($otherUser) {
            return $otherUser;
        });
        
        $this->assertFalse($request->authorize());
    }
}
            

Feature Testing with Form Requests


namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserManagementTest extends TestCase
{
    use RefreshDatabase;
    
    /** @test */
    public function user_can_be_created_with_valid_data()
    {
        $admin = factory(User::class)->create(['role' => 'admin']);
        
        $response = $this->actingAs($admin)->post('/users', [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password123',
            'password_confirmation' => 'password123',
            'role' => 'user',
        ]);
        
        $response->assertRedirect(route('users.index'));
        $this->assertDatabaseHas('users', [
            'name' => 'John Doe',
            'email' => 'john@example.com',
        ]);
    }
    
    /** @test */
    public function validation_errors_are_shown_for_invalid_data()
    {
        $admin = factory(User::class)->create(['role' => 'admin']);
        
        $response = $this->actingAs($admin)->post('/users', [
            'name' => '',
            'email' => 'not-valid-email',
            'password' => 'short',
            'password_confirmation' => 'different',
            'role' => 'invalid-role',
        ]);
        
        $response->assertSessionHasErrors(['name', 'email', 'password', 'role']);
    }
    
    /** @test */
    public function regular_users_cannot_create_users()
    {
        $user = factory(User::class)->create(['role' => 'user']);
        
        $response = $this->actingAs($user)->post('/users', [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password123',
            'password_confirmation' => 'password123',
            'role' => 'user',
        ]);
        
        $response->assertStatus(403); // Forbidden
    }
}
            

Testing form requests helps ensure that your validation and authorization logic works correctly. It's like having a quality control system that verifies each security component is functioning as expected.

Advanced Form Request Techniques

Custom Validation Keys


/**
 * Get the validation rules that apply to the request.
 */
public function rules(): array
{
    // Using the "dot" notation for nested arrays
    return [
        'users.*.name' => 'required|string|max:255',
        'users.*.email' => 'required|email|distinct',
        'config.site_name' => 'required|string|max:100',
        'config.theme' => 'required|in:light,dark,auto',
    ];
}

/**
 * Get custom messages for nested fields.
 */
public function messages(): array
{
    return [
        'users.*.name.required' => 'Each user must have a name.',
        'users.*.email.distinct' => 'Each user must have a unique email address.',
        'config.site_name.required' => 'The site name is required.',
    ];
}
            

Delaying Validation with validateResolved


/**
 * Handle a passed validation attempt.
 *
 * @return void
 */
protected function passedValidation()
{
    if (!$this->has('validated')) {
        // Store original data
        $originalData = $this->all();
        
        // Replace with validated data
        $validatedData = $this->validator->validated();
        $this->replace($validatedData);
        
        // Set a flag to prevent infinite recursion
        $this->merge(['validated' => true]);
        
        // Re-validate with additional rules
        $this->validateResolved();
        
        // Restore original data with additional validated fields
        $this->replace(array_merge($originalData, $this->except('validated')));
    }
}

/**
 * Get the validation rules that apply to the request.
 */
public function rules(): array
{
    $rules = [
        'name' => 'required|string|max:255',
        'email' => 'required|email',
    ];
    
    // Add additional rules after initial validation
    if ($this->has('validated')) {
        // Apply more complex rules that depend on the initial validation passing
        $rules['password'] = 'required|min:8|confirmed';
        
        // Example of a rule that needs access to validated data
        if ($this->name === 'Admin') {
            $rules['admin_code'] = 'required|string|exists:admin_codes,code';
        }
    }
    
    return $rules;
}
            

Custom Response for Failed Validation


namespace App\Http\Requests;

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

class ApiFormRequest extends FormRequest
{
    /**
     * Handle a failed validation attempt.
     *
     * @param  \Illuminate\Contracts\Validation\Validator  $validator
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    protected function failedValidation(Validator $validator)
    {
        if ($this->expectsJson()) {
            throw new HttpResponseException(
                response()->json([
                    'message' => 'Validation failed',
                    'errors' => $validator->errors(),
                    'data' => null
                ], 422)
            );
        }
        
        throw (new ValidationException($validator))
                ->errorBag($this->errorBag)
                ->redirectTo($this->getRedirectUrl());
    }
    
    /**
     * Handle a failed authorization attempt.
     *
     * @return void
     *
     * @throws \Illuminate\Http\Exceptions\HttpResponseException
     */
    protected function failedAuthorization()
    {
        if ($this->expectsJson()) {
            throw new HttpResponseException(
                response()->json([
                    'message' => 'You do not have permission to perform this action',
                    'errors' => [
                        'permission' => ['Unauthorized action']
                    ],
                    'data' => null
                ], 403)
            );
        }
        
        abort(403, 'You do not have permission to perform this action');
    }
}
            

These advanced techniques give you more control over the validation process, allowing you to implement sophisticated validation workflows and custom error handling.

Integration with Frontend Frameworks

Modern web applications often use JavaScript frameworks for the frontend. Let's explore how to integrate Laravel form validation with popular frameworks:

Creating a Validation API Endpoint


// routes/api.php
Route::post('/validate/{formRequest}', 'ValidationController@validate');

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

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class ValidationController extends Controller
{
    public function validate(Request $request, $formRequest)
    {
        // Get the form request class
        $className = 'App\\Http\\Requests\\' . $formRequest;
        
        if (!class_exists($className)) {
            return response()->json([
                'message' => 'Form request not found',
            ], 404);
        }
        
        // Create an instance of the form request
        $formRequestInstance = new $className();
        
        // Get the rules
        $rules = $formRequestInstance->rules();
        
        // Validate the request
        $validator = Validator::make($request->all(), $rules);
        
        if ($validator->fails()) {
            return response()->json([
                'valid' => false,
                'errors' => $validator->errors(),
            ], 422);
        }
        
        return response()->json([
            'valid' => true,
        ]);
    }
}
            

Vue.js Integration Example


// UserForm.vue
<template>
  <form @submit.prevent="submitForm">
    <div class="form-group">
      <label for="name">Name</label>
      <input 
        type="text" 
        id="name" 
        v-model="form.name" 
        @blur="validateField('name')"
        :class="{ 'is-invalid': errors.name }"
      >
      <div v-if="errors.name" class="invalid-feedback">
        {{ errors.name[0] }}
      </div>
    </div>
    
    <div class="form-group">
      <label for="email">Email</label>
      <input 
        type="email" 
        id="email" 
        v-model="form.email" 
        @blur="validateField('email')"
        :class="{ 'is-invalid': errors.email }"
      >
      <div v-if="errors.email" class="invalid-feedback">
        {{ errors.email[0] }}
      </div>
    </div>
    
    <div class="form-group">
      <label for="password">Password</label>
      <input 
        type="password" 
        id="password" 
        v-model="form.password" 
        @blur="validateField('password')"
        :class="{ 'is-invalid': errors.password }"
      >
      <div v-if="errors.password" class="invalid-feedback">
        {{ errors.password[0] }}
      </div>
    </div>
    
    <div class="form-group">
      <label for="password_confirmation">Confirm Password</label>
      <input 
        type="password" 
        id="password_confirmation" 
        v-model="form.password_confirmation" 
        @blur="validateField('password_confirmation')"
        :class="{ 'is-invalid': errors.password_confirmation }"
      >
      <div v-if="errors.password_confirmation" class="invalid-feedback">
        {{ errors.password_confirmation[0] }}
      </div>
    </div>
    
    <button type="submit" :disabled="isSubmitting">Register</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      form: {
        name: '',
        email: '',
        password: '',
        password_confirmation: ''
      },
      errors: {},
      isSubmitting: false
    }
  },
  methods: {
    async validateField(field) {
      try {
        const response = await axios.post('/api/validate/StoreUserRequest', {
          [field]: this.form[field],
          ...(field === 'password_confirmation' ? { password: this.form.password } : {})
        });
        
        // Clear the error for this field if validation passed
        if (this.errors[field]) {
          this.$delete(this.errors, field);
        }
      } catch (error) {
        if (error.response && error.response.status === 422) {
          // Update the errors object with only the validated field's errors
          if (error.response.data.errors[field]) {
            this.$set(this.errors, field, error.response.data.errors[field]);
          } else {
            this.$delete(this.errors, field);
          }
        }
      }
    },
    
    async validateForm() {
      try {
        await axios.post('/api/validate/StoreUserRequest', this.form);
        return true;
      } catch (error) {
        if (error.response && error.response.status === 422) {
          this.errors = error.response.data.errors;
        }
        return false;
      }
    },
    
    async submitForm() {
      this.isSubmitting = true;
      
      const isValid = await this.validateForm();
      
      if (isValid) {
        try {
          const response = await axios.post('/api/users', this.form);
          
          // Handle successful registration
          this.$router.push('/login');
          
        } catch (error) {
          if (error.response && error.response.status === 422) {
            this.errors = error.response.data.errors;
          } else {
            // Handle other errors
            console.error('An error occurred:', error);
          }
        }
      }
      
      this.isSubmitting = false;
    }
  }
}
</script>
            

React Integration Example


// UserForm.jsx
import React, { useState } from 'react';
import axios from 'axios';

const UserForm = () => {
  const [form, setForm] = useState({
    name: '',
    email: '',
    password: '',
    password_confirmation: ''
  });
  
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm(prevForm => ({
      ...prevForm,
      [name]: value
    }));
  };
  
  const validateField = async (field) => {
    try {
      const dataToValidate = {
        [field]: form[field]
      };
      
      // For password confirmation, include the password
      if (field === 'password_confirmation') {
        dataToValidate.password = form.password;
      }
      
      await axios.post('/api/validate/StoreUserRequest', dataToValidate);
      
      // Clear the error for this field if validation passed
      if (errors[field]) {
        setErrors(prevErrors => {
          const newErrors = { ...prevErrors };
          delete newErrors[field];
          return newErrors;
        });
      }
    } catch (error) {
      if (error.response && error.response.status === 422) {
        const fieldErrors = error.response.data.errors[field];
        
        if (fieldErrors) {
          setErrors(prevErrors => ({
            ...prevErrors,
            [field]: fieldErrors
          }));
        } else {
          // Clear error if it no longer exists
          setErrors(prevErrors => {
            const newErrors = { ...prevErrors };
            delete newErrors[field];
            return newErrors;
          });
        }
      }
    }
  };
  
  const validateForm = async () => {
    try {
      await axios.post('/api/validate/StoreUserRequest', form);
      return true;
    } catch (error) {
      if (error.response && error.response.status === 422) {
        setErrors(error.response.data.errors);
      }
      return false;
    }
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    const isValid = await validateForm();
    
    if (isValid) {
      try {
        const response = await axios.post('/api/users', form);
        
        // Handle successful registration
        window.location.href = '/login';
        
      } catch (error) {
        if (error.response && error.response.status === 422) {
          setErrors(error.response.data.errors);
        } else {
          // Handle other errors
          console.error('An error occurred:', error);
        }
      }
    }
    
    setIsSubmitting(false);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input 
          type="text" 
          id="name" 
          name="name"
          value={form.name}
          onChange={handleChange}
          onBlur={() => validateField('name')}
          className={`form-control ${errors.name ? 'is-invalid' : ''}`}
        />
        {errors.name && (
          <div className="invalid-feedback">
            {errors.name[0]}
          </div>
        )}
      </div>
      
      <!-- Other form fields -->
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Registering...' : 'Register'}
      </button>
    </form>
  );
};

export default UserForm;
            

These integration examples demonstrate how to leverage Laravel's form validation on the frontend, providing a seamless user experience with consistent validation between the frontend and backend. It's like having the same security personnel checking credentials at both the entrance gate and the inner doors - the rules are applied consistently wherever the user interacts with your application.

Real-World Example: Multi-Step Registration Process

Let's implement a comprehensive multi-step registration process with form request composition and validation:

graph LR A[Step 1: Personal Info] --> B[Step 2: Address] B --> C[Step 3: Account Setup] C --> D[Step 4: Confirmation]

Base Registration Request


// app/Http/Requests/Registration/BaseRegistrationRequest.php
namespace App\Http\Requests\Registration;

use Illuminate\Foundation\Http\FormRequest;

abstract class BaseRegistrationRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        // Everyone can register
        return true;
    }
    
    /**
     * Store registration data in session.
     *
     * @param string $key
     * @param array $data
     * @return void
     */
    protected function storeInSession(string $key, array $data): void
    {
        $registrationData = $this->session()->get('registration_data', []);
        $registrationData[$key] = $data;
        $this->session()->put('registration_data', $registrationData);
    }
    
    /**
     * Get registration data from session.
     *
     * @param string|null $key
     * @return mixed
     */
    protected function getFromSession(?string $key = null)
    {
        if ($key === null) {
            return $this->session()->get('registration_data', []);
        }
        
        $registrationData = $this->session()->get('registration_data', []);
        return $registrationData[$key] ?? null;
    }
    
    /**
     * Check if a step has been completed.
     *
     * @param string $step
     * @return bool
     */
    protected function stepCompleted(string $step): bool
    {
        $registrationData = $this->session()->get('registration_data', []);
        return isset($registrationData[$step]);
    }
    
    /**
     * Clear all registration data from session.
     *
     * @return void
     */
    protected function clearRegistrationData(): void
    {
        $this->session()->forget('registration_data');
    }
}
            

Step 1: Personal Information Request


// app/Http/Requests/Registration/StepOneRequest.php
namespace App\Http\Requests\Registration;

class StepOneRequest extends BaseRegistrationRequest
{
    /**
     * Get the validation rules that apply to the request.
     */
    public function rules(): array
    {
        return [
            'first_name' => 'required|string|max:50',
            'last_name' => 'required|string|max:50',
            'date_of_birth' => 'required|date|before:today|after:1900-01-01',
            'gender' => 'required|in:male,female,other,prefer_not_to_say',
        ];
    }
    
    /**
     * Store the validated data in session and proceed to step 2.
     */
    public function proceed()
    {
        $validatedData = $this->validated();
        
        // Store in session
        $this->storeInSession('personal', $validatedData);
        
        return redirect()->route('registration.step2');
    }
}
            

Step 2: Address Request


// app/Http/Requests/Registration/StepTwoRequest.php
namespace App\Http\Requests\Registration;

class StepTwoRequest extends BaseRegistrationRequest
{
    /**
     * Check if the previous step was completed.
     */
    public function authorize(): bool
    {
        return $this->stepCompleted('personal');
    }
    
    /**
     * Get the validation rules that apply to the request.
     */
    public function rules(): array
    {
        return [
            'address_line1' => 'required|string|max:100',
            'address_line2' => 'nullable|string|max:100',
            'city' => 'required|string|max:50',
            'state' => 'required|string|max:50',
            'zip_code' => 'required|string|max:20',
            'country' => 'required|string|max:50',
            'phone' => 'required|string|regex:/^([0-9\s\-\+\(\)]*)$/|min:10',
        ];
    }
    
    /**
     * Store the validated data in session and proceed to step 3.
     */
    public function proceed()
    {
        $validatedData = $this->validated();
        
        // Store in session
        $this->storeInSession('address', $validatedData);
        
        return redirect()->route('registration.step3');
    }
}
            

Step 3: Account Setup Request


// app/Http/Requests/Registration/StepThreeRequest.php
namespace App\Http\Requests\Registration;

use App\Rules\StrongPassword;
use Illuminate\Validation\Rules\Password;

class StepThreeRequest extends BaseRegistrationRequest
{
    /**
     * Check if the previous steps were completed.
     */
    public function authorize(): bool
    {
        return $this->stepCompleted('personal') && $this->stepCompleted('address');
    }
    
    /**
     * Get the validation rules that apply to the request.
     */
    public function rules(): array
    {
        return [
            'email' => 'required|string|email|max:100|unique:users',
            'username' => 'required|string|min:5|max:20|unique:users|alpha_dash',
            'password' => [
                'required',
                'confirmed',
                Password::min(8)
                    ->letters()
                    ->mixedCase()
                    ->numbers()
                    ->symbols()
                    ->uncompromised(),
            ],
            'terms_accepted' => 'required|accepted',
        ];
    }
    
    /**
     * Custom messages for validation.
     */
    public function messages(): array
    {
        return [
            'terms_accepted.required' => 'You must accept the terms and conditions to proceed.',
            'terms_accepted.accepted' => 'You must accept the terms and conditions to proceed.',
        ];
    }
    
    /**
     * Store the validated data in session and proceed to confirmation.
     */
    public function proceed()
    {
        $validatedData = $this->validated();
        
        // Remove password confirmation, we don't need to store it
        unset($validatedData['password_confirmation']);
        
        // Store in session
        $this->storeInSession('account', $validatedData);
        
        return redirect()->route('registration.confirmation');
    }
}
            

Final Submission Request


// app/Http/Requests/Registration/FinalSubmissionRequest.php
namespace App\Http\Requests\Registration;

use App\Models\User;
use App\Models\UserProfile;
use App\Models\Address;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;

class FinalSubmissionRequest extends BaseRegistrationRequest
{
    /**
     * Check if all previous steps were completed.
     */
    public function authorize(): bool
    {
        return $this->stepCompleted('personal') && 
               $this->stepCompleted('address') && 
               $this->stepCompleted('account');
    }
    
    /**
     * No additional rules for the final submission.
     */
    public function rules(): array
    {
        return [];
    }
    
    /**
     * Complete the registration process.
     */
    public function complete()
    {
        // Get all registration data
        $personal = $this->getFromSession('personal');
        $address = $this->getFromSession('address');
        $account = $this->getFromSession('account');
        
        // Start transaction
        DB::beginTransaction();
        
        try {
            // Create the user
            $user = new User();
            $user->email = $account['email'];
            $user->username = $account['username'];
            $user->password = Hash::make($account['password']);
            $user->save();
            
            // Create the profile
            $profile = new UserProfile();
            $profile->user_id = $user->id;
            $profile->first_name = $personal['first_name'];
            $profile->last_name = $personal['last_name'];
            $profile->date_of_birth = $personal['date_of_birth'];
            $profile->gender = $personal['gender'];
            $profile->phone = $address['phone'];
            $profile->save();
            
            // Create the address
            $userAddress = new Address();
            $userAddress->user_id = $user->id;
            $userAddress->address_line1 = $address['address_line1'];
            $userAddress->address_line2 = $address['address_line2'] ?? null;
            $userAddress->city = $address['city'];
            $userAddress->state = $address['state'];
            $userAddress->zip_code = $address['zip_code'];
            $userAddress->country = $address['country'];
            $userAddress->is_primary = true;
            $userAddress->save();
            
            // Commit the transaction
            DB::commit();
            
            // Clear registration data
            $this->clearRegistrationData();
            
            // Log in the user
            auth()->login($user);
            
            return redirect()->route('dashboard')
                ->with('success', 'Registration completed successfully!');
                
        } catch (\Exception $e) {
            // Rollback the transaction
            DB::rollBack();
            
            return redirect()->route('registration.step1')
                ->with('error', 'An error occurred during registration. Please try again.');
        }
    }
}
            

Registration Controller


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

use App\Http\Requests\Registration\StepOneRequest;
use App\Http\Requests\Registration\StepTwoRequest;
use App\Http\Requests\Registration\StepThreeRequest;
use App\Http\Requests\Registration\FinalSubmissionRequest;
use Illuminate\Http\Request;

class RegistrationController extends Controller
{
    /**
     * Show step 1 form.
     */
    public function showStepOne(Request $request)
    {
        $data = $request->session()->get('registration_data.personal', []);
        return view('registration.step1', compact('data'));
    }
    
    /**
     * Process step 1 form.
     */
    public function processStepOne(StepOneRequest $request)
    {
        return $request->proceed();
    }
    
    /**
     * Show step 2 form.
     */
    public function showStepTwo(Request $request)
    {
        // Check if step 1 was completed
        if (!$request->session()->has('registration_data.personal')) {
            return redirect()->route('registration.step1')
                ->with('error', 'Please complete the personal information form first.');
        }
        
        $data = $request->session()->get('registration_data.address', []);
        return view('registration.step2', compact('data'));
    }
    
    /**
     * Process step 2 form.
     */
    public function processStepTwo(StepTwoRequest $request)
    {
        return $request->proceed();
    }
    
    /**
     * Show step 3 form.
     */
    public function showStepThree(Request $request)
    {
        // Check if previous steps were completed
        if (!$request->session()->has('registration_data.personal') || 
            !$request->session()->has('registration_data.address')) {
            
            return redirect()->route('registration.step1')
                ->with('error', 'Please complete the previous steps first.');
        }
        
        $data = $request->session()->get('registration_data.account', []);
        return view('registration.step3', compact('data'));
    }
    
    /**
     * Process step 3 form.
     */
    public function processStepThree(StepThreeRequest $request)
    {
        return $request->proceed();
    }
    
    /**
     * Show confirmation page.
     */
    public function showConfirmation(Request $request)
    {
        // Check if all steps were completed
        if (!$request->session()->has('registration_data.personal') || 
            !$request->session()->has('registration_data.address') ||
            !$request->session()->has('registration_data.account')) {
            
            return redirect()->route('registration.step1')
                ->with('error', 'Please complete all steps before confirmation.');
        }
        
        $data = $request->session()->get('registration_data');
        return view('registration.confirmation', compact('data'));
    }
    
    /**
     * Complete registration.
     */
    public function complete(FinalSubmissionRequest $request)
    {
        return $request->complete();
    }
}
            

Routes


// routes/web.php
Route::prefix('register')->name('registration.')->group(function () {
    Route::get('/step-1', [RegistrationController::class, 'showStepOne'])->name('step1');
    Route::post('/step-1', [RegistrationController::class, 'processStepOne'])->name('process-step1');
    
    Route::get('/step-2', [RegistrationController::class, 'showStepTwo'])->name('step2');
    Route::post('/step-2', [RegistrationController::class, 'processStepTwo'])->name('process-step2');
    
    Route::get('/step-3', [RegistrationController::class, 'showStepThree'])->name('step3');
    Route::post('/step-3', [RegistrationController::class, 'processStepThree'])->name('process-step3');
    
    Route::get('/confirmation', [RegistrationController::class, 'showConfirmation'])->name('confirmation');
    Route::post('/complete', [RegistrationController::class, 'complete'])->name('complete');
});
            

This comprehensive example demonstrates:

This pattern can be adapted for many multi-step workflows in your applications, providing a structured, maintainable approach to complex form handling.

Practice Activity

Form Request Composition Exercise

Create a set of reusable form request components for an e-commerce system:

  1. Create a base form request class with shared validation logic and helper methods
  2. Extend it with specialized form requests for product, category, and order validation
  3. Implement different validation rules for admin users vs. regular merchants
  4. Create custom validation rules for product SKUs and order numbers
  5. Add preprocessing to normalize input data before validation

Advanced Validation Challenge

Build a form request for a job application system that:

  1. Validates different sets of fields based on the job type (technical, management, etc.)
  2. Requires a portfolio for creative positions but makes it optional for others
  3. Implements custom validation for file uploads (resume, portfolio)
  4. Creates a two-phase validation process where basic info is validated first, then specialized fields
  5. Integrates with your application's permission system to determine which fields can be edited

Frontend Integration Project

Create a JavaScript module that integrates with Laravel's validation:

  1. Build a frontend validation system that uses the same rules as your backend
  2. Create a real-time validation endpoint that checks fields as users type
  3. Implement client-side validation that matches server-side rules
  4. Add visual feedback for validation states (valid, invalid, pending)
  5. Handle complex validation scenarios like conditional fields and interdependent validation

Summary

By mastering form request composition and advanced validation techniques, you can create robust, maintainable form handling systems for even the most complex applications.