Laravel Form Validation
Validation is Laravel's "security checkpoint"—all data entering the system must pass validation, and not a single piece of invalid data can slip through.
1. What You'll Learn
- Validation rules: required/email/unique/exists/file/image/custom
- Form Request validation class: make:request
- Custom Validation Rules: Closure Rules and Rule Objects
- Error Message Handling: $errors View Sharing and API JSON Responses
- Condition Validation and Validation of "Bail/Sometimes" Rules
2. A True Story of a Security Auditor
(1) Pain Point: User input causes data discrepancies
When Alice signed up for ShopMetrics, she entered "abc" for her email address, "123" for her phone number, and left the store name field blank—the system accepted everything, resulting in a database full of invalid data. A malicious user submitted a product with a negative price via the API to Bob's store, resulting in an order amount of -999 USD and causing the financial reports to crash. During a security audit, Charlie discovered that 17 endpoints had no input validation whatsoever.
(2) Solutions for Laravel Validation
Laravel validation intercepts data before it enters the system—a single array of rules can cover all validation logic, and the FormRequest class keeps controllers clean.
// Validation rules — 30 seconds to write, protects forever
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'price' => 'required|numeric|min:0',
]);
(3) Revenue
After Alice enabled verification, invalid registrations dropped to zero; Bob's product prices will never be negative again; and Charlie passed all 17 endpoints in his security audit.
3. Quick Reference for Validation Rules
(1) Common Rules
| Rule | Description | Example |
|---|---|---|
required |
Required | 'name' => 'required' |
email |
Email Format | 'email' => 'required|email' |
unique:table,column |
Unique | 'slug' => 'unique:shops,slug' |
exists:table,column |
Exists | 'shop_id' => 'exists:shops,id' |
min:n |
Minimum Value/Length | 'price' => 'numeric|min:0' |
max:n |
Maximum Value/Length | 'name' => 'string|max:255' |
numeric |
Number | 'quantity' => 'required|numeric' |
integer |
Integer | 'page' => 'integer|min:1' |
string |
String | 'name' => 'required|string' |
boolean |
Boolean | 'is_active' => 'boolean' |
date |
Date | 'published_at' => 'date' |
file |
File | 'logo' => 'file|max:2048' |
image |
Image file | 'photo' => 'image|mimes:jpeg,png' |
confirmed |
Secondary Confirmation | 'password' => 'confirmed' |
regex:pattern |
Regular Expressions | 'phone' => 'regex:/^[0-9]{10}$/' |
in:a,b,c |
Enumeration Value | 'status' => 'in:active,suspended' |
(2) Verify the pipeline process
flowchart TD
A[Request Input] --> B[Validate Rules]
B -->|Pass| C[Sanitized Data]
C --> D[Controller Logic]
B -->|Fail| E[Redirect Back with Errors]
E --> F["Display \$errors in View"]
(1) ▶ Example: ShopMetrics Product Creation Validation
// Inline validation in controller
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'sku' => 'required|string|unique:products,sku',
'price' => 'required|numeric|min:0.01|max:999999.99',
'stock' => 'required|integer|min:0',
'category_id' => 'required|exists:categories,id',
'description' => 'nullable|string|max:5000',
'is_active' => 'boolean',
]);
$product = Product::create($validated);
return redirect()->route('products.show', $product)
->with('success', 'Product created.');
}
Output:
// Execution Successful
4. Form Request Validation Class
(1) Create a Form Request
php artisan make:request StoreShopRequest
php artisan make:request UpdateShopRequest
(2) Defining Rules and Authorization
// app/Http/Requests/StoreShopRequest.php
class StoreShopRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->check() && auth()->user()->can('create', Shop::class);
}
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'slug' => 'required|string|unique:shops,slug',
'description' => 'nullable|string|max:5000',
'domain' => 'nullable|url',
'status' => 'in:active,suspended',
];
}
public function messages(): array
{
return [
'name.required' => 'Shop name is required.',
'slug.unique' => 'This URL slug is already taken.',
'domain.url' => 'Please enter a valid URL.',
];
}
}
(3) Using it in the controller
public function store(StoreShopRequest $request): RedirectResponse
{
// $request->validated() only contains validated data
$shop = Shop::create($request->validated());
return redirect()->route('shops.show', $shop);
}
| Dimension | Inline Validation | Form Request |
|---|---|---|
| Location | Within the controller method | Separate class file |
| Reusability | Low | ✅ Reusability across multiple controllers |
| Authorization Check | Manual | ✅ authorize() |
| Controller Code | Longer | Streamlined |
| Suitable for | Simple verification | Complex/reusable verification |
(1) ▶ Example: ShopMetrics StoreShopRequest
// app/Http/Requests/StoreShopRequest.php
class StoreShopRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->user()->role === 'tenant_owner';
}
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'slug' => 'required|alpha_dash|unique:shops,slug,NULL,id,tenant_id,' . tenant()->id,
'description' => 'nullable|string|max:5000',
'status' => 'sometimes|in:active,suspended',
];
}
public function messages(): array
{
return [
'slug.unique' => 'You already have a shop with this slug.',
'slug.alpha_dash' => 'Slug can only contain letters, numbers, dashes.',
];
}
protected function prepareForValidation(): void
{
$this->merge([
'tenant_id' => tenant()->id,
'slug' => Str::slug($this->slug ?? $this->name),
]);
}
}
Output:
// Execution Successful
5. Custom Validation Rules
(1) Closure Rules
// Inline custom rule
$validated = $request->validate([
'discount' => [
'required',
'numeric',
'min:0',
function (string $attribute, mixed $value, Closure $fail) {
if ($value > request('subtotal')) {
$fail('The discount cannot exceed the subtotal.');
}
},
],
]);
(2) Rule Object
php artisan make:rule ValidCouponCode
// app/Rules/ValidCouponCode.php
class ValidCouponCode implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$coupon = Coupon::where('code', $value)
->where('expires_at', '>', now())
->where('usage_limit', '>', DB::raw('usage_count'))
->first();
if (!$coupon) {
$fail('This coupon code is invalid or expired.');
}
}
}
// Usage
public function rules(): array
{
return [
'coupon_code' => ['nullable', 'string', new ValidCouponCode()],
];
}
| Method | Suitable Scenarios | Reusability |
|---|---|---|
| Closure | Simple one-time logic | Low |
| Rule Object | Complex/Reusable Logic | ✅ |
Rule::class Method |
Database-related validation | ✅ |
(3) Rule Class Methods
use Illuminate\Validation\Rule;
// Unique ignoring current model
'slug' => Rule::unique('shops', 'slug')->ignore($shop->id),
// In with dynamic values
'status' => Rule::in(['active', 'suspended', 'closed']),
// Exists with additional query
'shop_id' => Rule::exists('shops', 'id')->where(function ($query) {
$query->where('tenant_id', tenant()->id);
}),
(1) ▶ Example: ShopMetrics Order Validation Rules
// app/Http/Requests/StoreOrderRequest.php
class StoreOrderRequest extends FormRequest
{
public function rules(): array
{
return [
'items' => 'required|array|min:1',
'items.*.product_id' => [
'required',
'integer',
Rule::exists('products', 'id')->where('is_active', true),
],
'items.*.quantity' => 'required|integer|min:1|max:100',
'coupon_code' => ['nullable', 'string', new ValidCouponCode()],
'discount' => [
'sometimes',
'numeric',
'min:0',
function ($attribute, $value, $fail) {
if ($value > $this->input('subtotal', 0)) {
$fail('Discount cannot exceed subtotal.');
}
},
],
];
}
}
Output:
// Execution Successful
6. Handling Error Messages
(1) Web Page Display Errors
<!-- Display all errors -->
@if ($errors->any())
<div class="alert alert-error">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<!-- Display error for specific field -->
<input type="text" name="name" value="{{ old('name') }}"
class="{{ $errors->has('name') ? 'border-red-500' : '' }}">
@error('name')
<p class="text-red-500 text-sm">{{ $message }}</p>
@enderror
(2) API JSON Error Response
{
"message": "The given data was invalid.",
"errors": {
"name": ["The name field is required."],
"email": ["The email must be a valid email address."]
}
}
| Request Type | Validation Failure Behavior | Invalid Format |
|---|---|---|
| Web Forms | Redirect Back to Form + Display Errors | $errors View Variables |
| API JSON | Returns 422 + JSON | {"errors": {...}} |
| AJAX | Returns 422 + JSON | Same as API |
(1) ▶ Example: Handling Validation Errors in the ShopMetrics API
// app/Http/Controllers/Api/ShopController.php
public function store(StoreShopRequest $request): JsonResponse
{
$shop = Shop::create($request->validated());
return response()->json([
'message' => 'Shop created successfully.',
'data' => new ShopResource($shop),
], 201);
}
// Client receives 422 on validation failure:
// {
// "message": "The slug has already been taken.",
// "errors": {
// "slug": ["The slug has already been taken."]
// }
// }
Output:
// Execution Successful
7. Condition Validation
(1) Sometimes Rule
// Only validate if field is present
$validated = $request->validate([
'name' => 'required|string',
'notes' => 'sometimes|nullable|string|max:5000',
]);
// Conditional rules based on another field
Validator::make($data, [
'payment_method' => 'required|in:credit_card,bank_transfer',
'card_number' => 'required_if:payment_method,credit_card|numeric',
'bank_account' => 'required_if:payment_method,bank_transfer|numeric',
]);
(2) Bail Rules
// Stop validating after first failure on a field
$validated = $request->validate([
'email' => 'bail|required|email|unique:users',
// If required fails, email and unique won't run
]);
| Rule | Function | Use Case |
|---|---|---|
sometimes |
Validate only if the field exists | Optional field |
bail |
Stop after first failure | Expensive verification |
required_if |
Required field | Payment method → Card number |
required_unless |
Field is optional | — |
required_with |
Required | Confirm Password |
prohibited_if |
Condition Prohibited | — |
exclude_if |
Exclude conditions | Do not write to validated |
nullable |
Allows null | Optional field |
(1) ▶ Example: ShopMetrics Conditional Validation Scenario
// app/Http/Requests/UpdateSubscriptionRequest.php
class UpdateSubscriptionRequest extends FormRequest
{
public function rules(): array
{
return [
'plan_id' => 'required|exists:plans,id',
'payment_method' => 'required|in:credit_card,paypal,bank_transfer',
'card_number' => 'required_if:payment_method,credit_card|string|size:16',
'card_cvv' => 'required_if:payment_method,credit_card|string|size:3',
'paypal_email' => 'required_if:payment_method,paypal|email',
'bank_account' => 'required_if:payment_method,bank_transfer|string',
'coupon_code' => 'sometimes|nullable|string|max:50',
];
}
}
Output:
// Execution Successful
8. Comprehensive Example: The Complete ShopMetrics Verification Process
// ============================================
// Comprehensive: ShopMetrics Order Validation
// Covers: Form Request, custom rules, conditional, errors
// ============================================
// app/Rules/SufficientStock.php
class SufficientStock implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$productId = request()->input(str_replace('.quantity', '.product_id', $attribute));
$product = Product::find($productId);
if ($product && $value > $product->stock) {
$fail("Only {$product->stock} units available for {$product->name}.");
}
}
}
// app/Http/Requests/StoreOrderRequest.php
class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->check();
}
public function rules(): array
{
return [
'items' => 'required|array|min:1|max:50',
'items.*.product_id' => [
'required',
'integer',
Rule::exists('products', 'id')->where('is_active', true),
],
'items.*.quantity' => [
'required',
'integer',
'min:1',
'max:100',
new SufficientStock(),
],
'coupon_code' => 'sometimes|nullable|string|max:50',
'notes' => 'sometimes|nullable|string|max:1000',
'shipping_address.line1' => 'required|string|max:255',
'shipping_address.city' => 'required|string|max:100',
'shipping_address.zip' => 'required|string|max:20',
'shipping_address.country' => 'required|string|size:2',
];
}
public function messages(): array
{
return [
'items.required' => 'Your cart is empty.',
'items.min' => 'Add at least one item to place an order.',
'items.*.product_id.exists' => 'One of the selected products is unavailable.',
'shipping_address.line1.required' => 'Street address is required.',
];
}
}
// Controller stays clean
public function store(StoreOrderRequest $request): RedirectResponse
{
$order = $this->orderService->createFromRequest($request);
return redirect()->route('orders.show', $order)
->with('success', 'Order placed successfully!');
}
❓ FAQ
validate() and Form Request?validate() directly; for complex validation (10+ rules, reusability, or authorization checks), use Form Request. Form Request is the best practice.Rule::unique('shops', 'slug')->ignore($shop->id) or unique:shops,slug,{shop} format. The update validation must exclude the current record; otherwise, validation will always fail.{"message":"...","errors":{"field":["error message"]}}. The front end should parse the errors object based on the 422 status code to display field-level errors.prepareForValidation?validated().$this->postJson() to submit invalid data; verify that a 422 error and the error message $response->assertJsonValidationErrors('name') are returned. You can also use Validator::make() to test the rules directly.unique and exists rules query the database, which incurs a performance overhead. For high-frequency APIs, consider using bail to terminate the operation early or caching validation results. In most scenarios, performance is not an issue.📖 Summary
- Laravel validation intercepts data before it enters the system, offering a wide range of rules that can be combined
- The Form Request class encapsulates the validation logic, keeping the controller clean
- Closure rules are suitable for one-time logic, while Rule objects are suitable for reusable logic
- Redirects and displays an error message upon failed web authentication; the API returns a 422 JSON response
- "sometimes"/nullable: Handles optional fields; "bail": Terminates early
- prepareForValidation: Modifies the input data before validation
📝 Exercises
-
Basic Exercise (⭐): Create a
StoreProductRequestfor a ShopMetrics product, including validation rules forname,sku,price,stock, andcategory_id, to ensure that the SKU is unique and the price is a positive number. -
Advanced Exercise (⭐⭐): Create a
ValidCouponCoderule object to verify that a coupon has not expired and still has remaining uses. Apply this rule to aStoreOrderRequestand test the responses for valid and invalid coupons. -
Challenge (⭐⭐⭐): Implement the SufficientStock Rule (to verify that the order quantity does not exceed inventory) on the
items.*.quantityfields ofStoreOrderRequestto handle inventory contention issues when multiple items are ordered simultaneously.



