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.
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.
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:
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:
- Creating a base form request class with shared functionality
- Building step-specific form requests that extend the base request
- Implementing authorization checks to ensure step completion
- Using session storage to maintain state between steps
- Applying different validation rules for each step
- Using transactions to ensure data integrity
- Creating a clean controller structure that leverages form requests
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:
- Create a base form request class with shared validation logic and helper methods
- Extend it with specialized form requests for product, category, and order validation
- Implement different validation rules for admin users vs. regular merchants
- Create custom validation rules for product SKUs and order numbers
- Add preprocessing to normalize input data before validation
Advanced Validation Challenge
Build a form request for a job application system that:
- Validates different sets of fields based on the job type (technical, management, etc.)
- Requires a portfolio for creative positions but makes it optional for others
- Implements custom validation for file uploads (resume, portfolio)
- Creates a two-phase validation process where basic info is validated first, then specialized fields
- 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:
- Build a frontend validation system that uses the same rules as your backend
- Create a real-time validation endpoint that checks fields as users type
- Implement client-side validation that matches server-side rules
- Add visual feedback for validation states (valid, invalid, pending)
- Handle complex validation scenarios like conditional fields and interdependent validation
Summary
- Form Request classes provide a powerful way to encapsulate validation and authorization logic
- The form request lifecycle includes hooks for preprocessing, validation, and post-validation handling
- You can implement sophisticated authorization checks within form requests
- Advanced validation techniques allow for dynamic rules based on context
- Form requests can be composed using traits, inheritance, and custom rule objects
- Testing form requests ensures your validation logic works correctly
- Advanced techniques like delayed validation and custom responses provide flexibility
- Frontend frameworks can integrate with Laravel validation through APIs
- Multi-step forms can be implemented with form requests and session storage
By mastering form request composition and advanced validation techniques, you can create robust, maintainable form handling systems for even the most complex applications.