Form Handling & Validation
Form Handling & Validation
Forms are the primary way users interact with web applications. Laravel provides powerful tools for handling form submissions, validating input, and displaying errors. In this lesson, you'll learn how to build secure, validated forms with elegant error handling.
Why Validation Matters
Never trust user input. Validation ensures that:
- Data meets your requirements before processing
- Your database stays clean and consistent
- Your application is protected from malicious input
- Users receive clear feedback about errors
CSRF Protection
Laravel automatically protects your application from Cross-Site Request Forgery (CSRF) attacks. Every form that uses POST, PUT, PATCH, or DELETE must include a CSRF token.
<form action="/posts" method="POST">
@csrf
<!-- Form fields -->
<button type="submit">Submit</button>
</form>
@csrf, your POST/PUT/PATCH/DELETE requests will be rejected with a 419 error. Always include it in your forms.
Form Method Spoofing
HTML forms only support GET and POST methods. To use PUT, PATCH, or DELETE, you need to spoof the method:
<!-- Update form (PUT) -->
<form action="/posts/{{ $post->id }}" method="POST">
@csrf
@method('PUT')
<!-- Form fields -->
</form>
<!-- Delete form -->
<form action="/posts/{{ $post->id }}" method="POST">
@csrf
@method('DELETE')
<button type="submit">Delete</button>
</form>
Basic Validation in Controllers
The simplest way to validate is directly in your controller using the validate() method:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Post;
class PostController extends Controller
{
public function store(Request $request)
{
// Validate the request
$validated = $request->validate([
'title' => 'required|max:255',
'content' => 'required',
'published_at' => 'nullable|date',
]);
// If validation passes, create the post
$post = Post::create($validated);
return redirect()->route('posts.show', $post)
->with('success', 'Post created successfully!');
}
}
If validation fails, Laravel automatically redirects back with errors and old input. No additional code needed!
Common Validation Rules
// Required & Optional
'field' => 'required' // Must be present and not empty
'field' => 'nullable' // Can be null
'field' => 'sometimes' // Only validate if present
// String Rules
'field' => 'string' // Must be string
'field' => 'max:255' // Max 255 characters
'field' => 'min:3' // Min 3 characters
'field' => 'between:3,255' // Between 3-255 characters
// Numeric Rules
'field' => 'integer' // Must be integer
'field' => 'numeric' // Must be numeric (int/float)
'field' => 'min:1' // Min value 1
'field' => 'max:100' // Max value 100
'field' => 'between:1,100' // Between 1-100
// Date Rules
'field' => 'date' // Valid date
'field' => 'date_format:Y-m-d' // Specific format
'field' => 'before:tomorrow' // Before tomorrow
'field' => 'after:yesterday' // After yesterday
'field' => 'before_or_equal:today'
'field' => 'after_or_equal:2023-01-01'
// Email & URL
'field' => 'email' // Valid email
'field' => 'email:rfc,dns' // Stricter email validation
'field' => 'url' // Valid URL
'field' => 'active_url' // URL that responds
// Boolean
'field' => 'boolean' // true, false, 1, 0, "1", "0"
'field' => 'accepted' // yes, on, 1, true (for T&C)
// Arrays
'field' => 'array' // Must be array
'field' => 'array:key1,key2' // Array with specific keys
'field.*' => 'string' // Each array item is string
// File Upload
'field' => 'file' // Must be file
'field' => 'image' // Must be image
'field' => 'mimes:jpg,png,pdf' // Specific MIME types
'field' => 'max:2048' // Max 2MB (in kilobytes)
'field' => 'dimensions:min_width=100,min_height=100'
// Database Rules
'field' => 'unique:users,email' // Unique in users.email
'field' => 'exists:users,id' // Exists in users.id
'field' => 'unique:users,email,' . $user->id // Ignore current user
Multiple Rules
Combine multiple rules using pipe separator or array syntax:
// Pipe syntax
$request->validate([
'email' => 'required|email|unique:users,email',
'password' => 'required|min:8|confirmed',
]);
// Array syntax (recommended for complex rules)
$request->validate([
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'min:8', 'confirmed'],
]);
Custom Validation Messages
Override default error messages:
$request->validate(
[
'title' => 'required|max:255',
'email' => 'required|email|unique:users',
],
[
'title.required' => 'Please enter a title for your post.',
'title.max' => 'The title cannot be longer than 255 characters.',
'email.required' => 'We need your email address.',
'email.unique' => 'This email is already registered.',
]
);
Custom Attribute Names
Customize attribute names in error messages:
$request->validate(
[
'user_email' => 'required|email',
],
[],
[
'user_email' => 'email address',
]
);
// Error message: "The email address field is required."
// Instead of: "The user email field is required."
Displaying Validation Errors
Laravel automatically makes errors available to your views:
<!-- Display all errors -->
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<!-- Display error for specific field -->
<input type="text" name="title" value="{{ old('title') }}">
@error('title')
<div class="error">{{ $message }}</div>
@enderror
<!-- Check if field has error -->
<input type="email"
name="email"
class="@error('email') is-invalid @enderror"
value="{{ old('email') }}">
Preserving Old Input
When validation fails, preserve user input with the old() helper:
<input type="text" name="title" value="{{ old('title') }}">
<textarea name="content">{{ old('content') }}</textarea>
<select name="category">
<option value="1" {{ old('category') == 1 ? 'selected' : '' }}>Tech</option>
<option value="2" {{ old('category') == 2 ? 'selected' : '' }}>News</option>
</select>
<input type="checkbox"
name="terms"
{{ old('terms') ? 'checked' : '' }}>
Form Request Validation
For complex validation logic, create a dedicated Form Request class:
# Create a form request
php artisan make:request StorePostRequest
This creates app/Http/Requests/StorePostRequest.php:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
// Return true to allow all users
// Or add authorization logic
return true;
}
/**
* Get the validation rules.
*/
public function rules(): array
{
return [
'title' => 'required|max:255',
'content' => 'required',
'category_id' => 'required|exists:categories,id',
'tags' => 'array',
'tags.*' => 'exists:tags,id',
'published_at' => 'nullable|date|after:now',
'featured_image' => 'nullable|image|max:2048',
];
}
/**
* Custom error messages.
*/
public function messages(): array
{
return [
'title.required' => 'Please provide a title for your post.',
'category_id.exists' => 'The selected category is invalid.',
];
}
/**
* Custom attribute names.
*/
public function attributes(): array
{
return [
'category_id' => 'category',
'featured_image' => 'cover image',
];
}
}
Using Form Request in controller:
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StorePostRequest;
use App\Models\Post;
class PostController extends Controller
{
public function store(StorePostRequest $request)
{
// Validation happens automatically before this method runs
// If validation fails, user is redirected with errors
// Get only validated data
$validated = $request->validated();
$post = Post::create($validated);
return redirect()->route('posts.show', $post)
->with('success', 'Post created!');
}
}
Conditional Validation
Add rules based on conditions:
use Illuminate\Validation\Rule;
$request->validate([
'role' => 'required|in:user,admin',
// Only validate permissions if role is admin
'permissions' => Rule::requiredIf(fn () => $request->role === 'admin'),
// Only validate if field is present
'website' => 'sometimes|url',
// Exclude if condition is true
'password' => 'exclude_if:oauth_login,true|required|min:8',
]);
Nested Array Validation
Validate nested arrays and objects:
// Form with multiple products
$request->validate([
'products' => 'required|array|min:1',
'products.*.name' => 'required|string|max:255',
'products.*.price' => 'required|numeric|min:0',
'products.*.quantity' => 'required|integer|min:1',
]);
// Nested objects
$request->validate([
'user.name' => 'required|string',
'user.email' => 'required|email',
'user.address.street' => 'required',
'user.address.city' => 'required',
]);
File Upload Validation
$request->validate([
// Basic file validation
'document' => 'required|file|max:10240', // Max 10MB
// Image validation
'photo' => 'required|image|mimes:jpeg,png,jpg|max:2048',
// Image dimensions
'avatar' => [
'required',
'image',
'dimensions:min_width=100,min_height=100,max_width=1000,max_height=1000'
],
// Multiple files
'photos' => 'required|array|min:1|max:5',
'photos.*' => 'image|max:2048',
]);
// Handling file upload
if ($request->hasFile('photo')) {
$path = $request->file('photo')->store('photos', 'public');
}
Create a contact form with validation:
- Create route, controller, and view for contact form
- Fields: name (required, max 100), email (required, valid email), subject (required, max 200), message (required, min 10)
- Add CSRF protection
- Display validation errors below each field
- Preserve old input on validation failure
- Show success message after submission
Create a Form Request for blog posts:
- Create
StorePostRequestandUpdatePostRequest - Validation: title (required, max 255), slug (required, unique), content (required, min 50), category_id (exists in categories), tags (array of existing tag IDs), featured_image (optional image, max 2MB)
- Custom messages for all validation rules
- Authorization: only authenticated users can create/update
- Implement in PostController
Create a product form with conditional validation:
- If product type is "physical": require weight, dimensions, shipping_cost
- If product type is "digital": require download_link, file_size
- All products require: name, price (min 0.01), description
- If "is_discounted" is checked: require discount_price (less than regular price)
- Allow multiple product images (1-5 images, each max 1MB)
Best Practices
- Always Validate User Input: Never trust data from forms, APIs, or any external source.
- Use Form Requests: Move complex validation logic to Form Request classes for better organization.
- Fail Fast: Place most restrictive rules first (e.g.,
requiredbeforeemail). - Provide Clear Error Messages: Users should understand exactly what went wrong and how to fix it.
- Preserve User Input: Always use
old()to preserve input on validation failures. - Validate File Uploads: Always validate file type, size, and dimensions to prevent security issues.
- Use CSRF Protection: Never disable CSRF protection unless you have a very good reason.
Summary
In this lesson, you've mastered:
- CSRF protection and form method spoofing
- Basic controller validation with common rules
- Custom validation messages and attribute names
- Displaying validation errors in views
- Preserving old input with the old() helper
- Creating Form Request classes for complex validation
- Conditional and nested array validation
- File upload validation and handling
Form validation is crucial for data integrity and security. With Laravel's validation system, you can build robust, user-friendly forms with minimal code!