Introduction to Laravel Validation
Data validation is a critical aspect of web application development. It ensures that incoming data meets your application's requirements before processing it. Laravel provides a robust, flexible validation system that makes it easy to validate incoming requests while keeping your code clean and organized.
Think of validation as the security checkpoint for your application's data. Just as an airport security checkpoint prevents prohibited items from entering, validation prevents invalid or malicious data from entering your application.
In the previous lecture, we introduced basic validation concepts. In this lecture, we'll delve deeper into Laravel's validation capabilities, exploring the wide range of built-in validation rules and how to customize them for your specific needs.
Validation Basics Review
Validation in Controllers
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string|min:10',
'published_at' => 'nullable|date',
]);
// Validation passed; proceed with creating the resource
$post = Post::create($validated);
return redirect()->route('posts.show', $post);
}
Using Form Request Classes
// Generate a Form Request
php artisan make:request StorePostRequest
// app/Http/Requests/StorePostRequest.php
class StorePostRequest extends FormRequest
{
public function authorize()
{
return true; // or use auth logic
}
public function rules()
{
return [
'title' => 'required|string|max:255',
'body' => 'required|string|min:10',
'published_at' => 'nullable|date',
];
}
}
// In the controller
public function store(StorePostRequest $request)
{
// Validation already happened
$post = Post::create($request->validated());
return redirect()->route('posts.show', $post);
}
These methods provide a foundation for validation in Laravel, but the system offers much more depth and flexibility than just these basics.
Available Validation Rules
Laravel provides an extensive set of validation rules. Here's an overview categorized by type:
Basic Validation Rules
required: Field must be present and not emptyrequired_if:anotherfield,value: Required if another field equals a valuerequired_unless:anotherfield,value: Required unless another field equals a valuerequired_with:foo,bar,...: Required if any of the other fields are presentrequired_with_all:foo,bar,...: Required if all of the other fields are presentrequired_without:foo,bar,...: Required if any of the other fields are not presentrequired_without_all:foo,bar,...: Required if all of the other fields are not presentnullable: Field can be nullpresent: Field must be present (can be empty)filled: Field must be present and not empty
String and Text Validation
string: Must be a stringalpha: Must contain only alphabetic charactersalpha_dash: Alphabetic characters, dashes, and underscoresalpha_num: Alphabetic characters and numbersstarts_with:foo,bar,...: Must start with one of the given valuesends_with:foo,bar,...: Must end with one of the given valuessize:value: String length must be exactly the given valuemin:value: Minimum string lengthmax:value: Maximum string lengthbetween:min,max: String length between valuesregex:pattern: Must match the regular expressionnot_regex:pattern: Must not match the regular expression
Numeric Validation
numeric: Must be numeric (integer or float/double)integer: Must be an integerdecimal:min,max: Must be a decimal with specified placesdigits:value: Must be numeric with exact lengthdigits_between:min,max: Numeric with length between valuesmin:value: Minimum numeric valuemax:value: Maximum numeric valuebetween:min,max: Numeric value between valuesgt:field: Greater than another fieldgte:field: Greater than or equal to another fieldlt:field: Less than another fieldlte:field: Less than or equal to another field
Date Validation
date: Must be a valid datedate_equals:date: Must be equal to the datedate_format:format: Must match the formatafter:date: Must be after the dateafter_or_equal:date: Must be after or equal to the datebefore:date: Must be before the datebefore_or_equal:date: Must be before or equal to the date
Array and Object Validation
array: Must be an arrayarray:foo,bar,...: Array must only contain specific keysjson: Must be a valid JSON stringdistinct: Array values must be uniquein:foo,bar,...: Value must be in the listnot_in:foo,bar,...: Value must not be in the list
File Validation
file: Must be a successfully uploaded fileimage: Must be an image (jpeg, png, bmp, gif, svg, or webp)mimes:jpeg,png,...: Must have a MIME type in the listmimetypes:video/avi,...: Must match one of the MIME typesdimensions: Image must meet the dimensions constraintsmax:value: Maximum file size in kilobytes
Database Related Validation
exists:table,column: Value must exist in the database tableunique:table,column,except,idColumn: Value must be unique in the database table
These rules can be combined to create sophisticated validation requirements for your forms. Think of them as building blocks for constructing a comprehensive validation strategy.
Advanced Rule Usage
Array Validation
// Validating a simple array
$rules = [
'tags' => 'required|array|min:1|max:5',
'tags.*' => 'string|max:50',
];
// Validating a nested array
$rules = [
'users' => 'required|array|min:1',
'users.*.name' => 'required|string|max:255',
'users.*.email' => 'required|email|unique:users,email',
'users.*.roles' => 'array',
'users.*.roles.*' => 'exists:roles,id',
];
The * notation allows you to validate each element of an array. This is particularly useful for forms that allow adding multiple items, like a product with multiple variations.
Custom Rule Arrays
// Using arrays for validation rules
$rules = [
'email' => ['required', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'min:8', 'confirmed'],
];
// With conditional rules
$rules = [
'email' => ['required', 'email', 'max:255'],
'role' => ['required', 'in:admin,user,editor'],
'permissions' => [
'required_if:role,admin',
'array',
],
];
Using arrays for rules makes your validation code more readable, especially for fields with many rules.
Rule Objects
For more complex validation rules, Laravel supports using rule objects:
use Illuminate\Validation\Rules\Password;
$rules = [
'password' => [
'required',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(),
],
];
The Password rule class provides a fluent interface for defining password requirements, including checking if the password has been compromised in data breaches using the Have I Been Pwned API.
File Validation Examples
// Basic file validation
$rules = [
'photo' => 'required|file|image|max:2048', // 2MB max
];
// Specific file types
$rules = [
'document' => 'required|file|mimes:pdf,doc,docx|max:10240', // 10MB max
];
// Image dimensions
$rules = [
'banner' => [
'required',
'image',
'dimensions:min_width=1200,min_height=300,max_width=2400,max_height=600',
],
];
// Multiple files
$rules = [
'photos' => 'required|array|min:1|max:5',
'photos.*' => 'image|max:2048',
];
These file validation rules help ensure that uploaded files meet your requirements for type, size, and dimensions before you attempt to process them.
Database Rules
// Basic exists and unique
$rules = [
'email' => 'required|email|unique:users,email',
'category_id' => 'required|exists:categories,id',
];
// Unique with exceptions (for updates)
$rules = [
'email' => 'required|email|unique:users,email,' . $user->id,
];
// More complex unique rule
$rules = [
'username' => [
'required',
Rule::unique('users')->ignore($user->id)->where(function ($query) {
return $query->where('active', true);
}),
],
];
// Exists with constraints
$rules = [
'category_id' => [
'required',
Rule::exists('categories', 'id')->where(function ($query) {
return $query->where('active', true);
}),
],
];
Database rules are essential for maintaining data integrity in your application. They ensure that referenced IDs exist and that unique constraints are respected.
Conditionally Adding Rules
Sometimes you need to apply validation rules conditionally based on other input data or application state:
Using sometimes
$rules = [
'middle_name' => 'sometimes|string|max:255',
];
The sometimes rule means the field will only be validated if it's present in the input data.
Using Validator::sometimes()
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'games' => 'required|numeric',
]);
$validator->sometimes('reason', 'required|max:500', function ($input) {
return $input->games >= 100;
});
This approach allows for more complex conditional logic. Here, the 'reason' field is only required when the 'games' value is 100 or more.
Using exclude_if / exclude_unless
$rules = [
'payment_type' => 'required|in:credit,paypal,bank',
'card_number' => 'exclude_if:payment_type,paypal,bank|required|string',
'paypal_email' => 'exclude_unless:payment_type,paypal|required|email',
'bank_account' => 'exclude_unless:payment_type,bank|required|string',
];
These rules help manage interdependent fields by excluding fields from validation based on conditions.
Building Rules Dynamically
public function rules()
{
$rules = [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
];
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
$userId = $this->route('user');
$rules['email'] = "required|email|unique:users,email,{$userId}";
$rules['password'] = 'sometimes|required|min:8|confirmed';
} else {
$rules['password'] = 'required|min:8|confirmed';
}
if ($this->input('role') === 'admin') {
$rules['permissions'] = 'required|array';
$rules['permissions.*'] = 'exists:permissions,id';
}
return $rules;
}
This approach allows you to build complex validation rules based on multiple conditions, such as the request method, route parameters, and input values.
Creating Custom Validation Rules
Laravel offers several ways to create custom validation rules when the built-in rules don't meet your needs:
Using Rule Objects
// Generate a rule object
php artisan make:rule StrongPassword
// app/Rules/StrongPassword.php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class StrongPassword implements Rule
{
public function passes($attribute, $value)
{
// Value must have at least 8 characters
if (strlen($value) < 8) {
return false;
}
// Value must contain at least one uppercase letter
if (!preg_match('/[A-Z]/', $value)) {
return false;
}
// Value must contain at least one lowercase letter
if (!preg_match('/[a-z]/', $value)) {
return false;
}
// Value must contain at least one number
if (!preg_match('/[0-9]/', $value)) {
return false;
}
// Value must contain at least one special character
if (!preg_match('/[^A-Za-z0-9]/', $value)) {
return false;
}
return true;
}
public function message()
{
return 'The :attribute must be at least 8 characters and include uppercase, lowercase, numbers, and special characters.';
}
}
// Using the rule
$request->validate([
'password' => ['required', new StrongPassword],
]);
Rule objects are ideal for complex validation logic that you want to reuse across your application. They encapsulate both the validation logic and the error message.
Using Closure Rules
$rules = [
'password' => [
'required',
function ($attribute, $value, $fail) {
if (strlen($value) < 8) {
$fail('The '.$attribute.' must be at least 8 characters.');
}
if (!preg_match('/[A-Z]/', $value)) {
$fail('The '.$attribute.' must contain at least one uppercase letter.');
}
// More checks...
},
],
];
Closure rules are convenient for one-off validations that are specific to a particular form or use case.
Using Validator::extend()
// In a service provider's boot method
Validator::extend('strong_password', function ($attribute, $value, $parameters, $validator) {
return strlen($value) >= 8 &&
preg_match('/[A-Z]/', $value) &&
preg_match('/[a-z]/', $value) &&
preg_match('/[0-9]/', $value) &&
preg_match('/[^A-Za-z0-9]/', $value);
});
// Define custom error message
Validator::replacer('strong_password', function ($message, $attribute, $rule, $parameters) {
return "The {$attribute} must be at least 8 characters and include uppercase, lowercase, numbers, and special characters.";
});
// Using the rule
$rules = [
'password' => 'required|strong_password',
];
This approach registers a custom rule globally, making it available throughout your application like any built-in rule.
Invokable Rule Classes
In Laravel 9+, you can use invokable rule classes:
// Generate an invokable rule
php artisan make:rule StrongPassword --invokable
// app/Rules/StrongPassword.php
namespace App\Rules;
use Illuminate\Contracts\Validation\InvokableRule;
class StrongPassword implements InvokableRule
{
public function __invoke($attribute, $value, $fail)
{
if (strlen($value) < 8) {
$fail('The '.$attribute.' must be at least 8 characters.');
}
if (!preg_match('/[A-Z]/', $value)) {
$fail('The '.$attribute.' must contain at least one uppercase letter.');
}
// More checks...
}
}
// Using the rule
$rules = [
'password' => ['required', new StrongPassword],
];
Invokable rules combine the maintainability of rule objects with the simplicity of closure rules.
Customizing Error Messages
Laravel provides several ways to customize validation error messages:
Custom Messages in validate()
$validated = $request->validate([
'title' => 'required|max:255',
'body' => 'required',
], [
'title.required' => 'A title is required',
'title.max' => 'Title cannot be more than 255 characters',
'body.required' => 'A message is required',
]);
Custom Messages in Form Requests
class StorePostRequest extends FormRequest
{
public function rules()
{
return [
'title' => 'required|max:255',
'body' => 'required',
];
}
public function messages()
{
return [
'title.required' => 'A title is required',
'title.max' => 'Title cannot be more than 255 characters',
'body.required' => 'A message is required',
];
}
// For specific attributes
public function attributes()
{
return [
'email' => 'email address',
];
}
}
Custom Messages for Specific Fields
$messages = [
'required' => 'The :attribute field is required',
'email.required' => 'We need your email address',
'password.min' => 'Password should be at least :min characters',
];
$validator = Validator::make($request->all(), $rules, $messages);
You can use placeholders in your messages to make them more dynamic:
:attribute- The name of the field being validated:min,:max, etc. - Rule parameters:values- For rules like 'in' to show valid options
Localized Messages
For multi-language applications, you can translate validation messages:
// resources/lang/en/validation.php
return [
'required' => 'The :attribute field is required.',
'email' => 'The :attribute must be a valid email address.',
// ...
'custom' => [
'email' => [
'required' => 'We need your email address',
],
],
'attributes' => [
'email' => 'email address',
],
];
Laravel will use these translations when generating validation error messages.
Manually Creating Validators
While the validate() method is convenient, sometimes you need more control over the validation process:
use Illuminate\Support\Facades\Validator;
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'title' => 'required|max:255',
'body' => 'required',
]);
if ($validator->fails()) {
return redirect('post/create')
->withErrors($validator)
->withInput();
}
// Validation passed
$validated = $validator->validated();
// Create the post
$post = Post::create($validated);
return redirect()->route('posts.show', $post);
}
The manual approach gives you more flexibility, such as adding conditional validation rules or performing actions even when validation fails.
Advanced Validator Methods
// Get all validation errors
$errors = $validator->errors();
// Get first error for a field
$firstError = $validator->errors()->first('email');
// Check if a specific field has errors
if ($validator->errors()->has('email')) {
// ...
}
// Get validated data
$validated = $validator->validated();
// Get parsed rules
$rules = $validator->getRules();
// Add custom errors
$validator->errors()->add('field', 'Custom error message');
// Execute validation for specific fields only
$validator->only(['name', 'email']);
// Skip validation for specific fields
$validator->except(['password', 'password_confirmation']);
// Check validation status without redirecting
if ($validator->fails()) {
$errors = $validator->errors();
// Process errors without redirection
}
These methods give you fine-grained control over the validation process.
After Validation Hook
$validator = Validator::make($request->all(), $rules);
$validator->after(function ($validator) use ($request) {
// Perform complex validation that needs to happen after all other rules
if ($request->input('password') === '123456') {
$validator->errors()->add('password', 'Your password cannot be 123456');
}
// Check database conditions
if (User::where('email', $request->email)->where('is_banned', true)->exists()) {
$validator->errors()->add('email', 'This account has been banned');
}
});
if ($validator->fails()) {
// Handle validation errors
}
The after() hook is perfect for validation logic that depends on multiple fields or requires database queries.
API Validation
When building APIs, you typically want to return validation errors as JSON instead of redirecting:
// In a controller
public function store(Request $request)
{
try {
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
$user = User::create($validated);
return response()->json([
'message' => 'User created successfully',
'user' => $user
], 201);
} catch (ValidationException $e) {
return response()->json([
'message' => 'Validation failed',
'errors' => $e->errors()
], 422);
}
}
However, Laravel automatically handles this for you when the request expects JSON:
// If the request wants a JSON response (has Accept: application/json header)
// Laravel will automatically return a JSON response with validation errors
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
$user = User::create($validated);
return response()->json([
'message' => 'User created successfully',
'user' => $user
], 201);
}
When validation fails, Laravel will return a JSON response like this:
{
"message": "The given data was invalid.",
"errors": {
"name": [
"The name field is required."
],
"email": [
"The email field is required."
]
}
}
Customizing API Validation Responses
// In a Form Request class
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(
response()->json([
'success' => false,
'message' => 'Validation errors',
'data' => $validator->errors()
], 422)
);
}
This allows you to customize the format of API validation error responses to match your API's conventions.
Form Validation with JavaScript
While server-side validation is essential, client-side validation provides immediate feedback to users. Laravel works well with JavaScript validation libraries:
HTML5 Validation Attributes
<!-- Using HTML5 attributes for client-side validation -->
<input type="email" name="email" required minlength="5" maxlength="255">
<input type="password" name="password" required minlength="8">
<input type="number" name="age" min="18" max="120">
<input type="url" name="website">
HTML5 validation provides basic client-side validation but should always be backed by server-side validation.
Laravel & JavaScript Validation
One approach is to pass validation rules to JavaScript:
<!-- In your Blade view -->
<form id="createUserForm" method="POST" action="{{ route('users.store') }}">
@csrf
<div class="form-group">
<input type="text" name="name" id="name" value="{{ old('name') }}">
</div>
<div class="form-group">
<input type="email" name="email" id="email" value="{{ old('email') }}">
</div>
<button type="submit">Create User</button>
</form>
<script>
// Pass Laravel validation rules to JavaScript
const validationRules = @json([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
]);
// Then use a JS validation library to apply these rules
// (This is pseudocode - implementation depends on your validation library)
setupValidator('createUserForm', validationRules);
</script>
This approach ensures your client-side and server-side validation rules stay in sync.
Using Laravel Form Request in JavaScript
// routes/web.php
Route::get('/validation-rules/{formRequest}', function ($formRequest) {
$class = "App\\Http\\Requests\\{$formRequest}";
if (!class_exists($class)) {
return response()->json(['error' => 'Form request not found'], 404);
}
$request = new $class;
return response()->json([
'rules' => $request->rules(),
'messages' => method_exists($request, 'messages') ? $request->messages() : [],
]);
})->middleware('auth');
// In your JavaScript
fetch('/validation-rules/StoreUserRequest')
.then(response => response.json())
.then(data => {
// Initialize client-side validation with these rules
setupValidator('form', data.rules, data.messages);
});
This more advanced approach lets you reuse your Form Request validation rules in your JavaScript code.
Real-World Example: Product Management Form
Let's implement a comprehensive product management form with complex validation requirements:
Form Request
// app/Http/Requests/StoreProductRequest.php
namespace App\Http\Requests;
use App\Rules\Barcode;
use Illuminate\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest
{
public function authorize()
{
return $this->user()->can('create', Product::class);
}
public function rules()
{
return [
// Basic Product Information
'name' => 'required|string|max:255',
'slug' => [
'nullable',
'string',
'max:255',
Rule::unique('products')->ignore($this->product),
'regex:/^[a-z0-9\-]+$/',
],
'description' => 'required|string|min:20',
'short_description' => 'nullable|string|max:255',
// Pricing and Inventory
'price' => 'required|numeric|min:0.01|max:999999.99',
'sale_price' => 'nullable|numeric|min:0.01|lt:price',
'cost' => 'nullable|numeric|min:0',
'sku' => [
'required',
'string',
'max:50',
Rule::unique('products')->ignore($this->product),
],
'barcode' => ['nullable', 'string', new Barcode],
'quantity' => 'required|integer|min:0',
'backorder' => 'boolean',
'requires_shipping' => 'boolean',
// Categorization
'category_id' => 'required|exists:categories,id',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
// Variants
'has_variants' => 'boolean',
'variants' => 'required_if:has_variants,true|array|min:1',
'variants.*.name' => 'required_with:variants|string|max:255',
'variants.*.sku' => [
'required_with:variants',
'string',
'max:50',
Rule::unique('product_variants', 'sku')->where(function ($query) {
$query->where('product_id', '!=', $this->product?->id);
}),
],
'variants.*.price' => 'required_with:variants|numeric|min:0.01',
'variants.*.quantity' => 'required_with:variants|integer|min:0',
// Media
'featured_image' => 'nullable|image|max:2048|dimensions:min_width=800,min_height=800',
'gallery' => 'nullable|array|max:5',
'gallery.*' => 'image|max:2048',
// SEO
'seo_title' => 'nullable|string|max:60',
'seo_description' => 'nullable|string|max:160',
'seo_keywords' => 'nullable|string|max:255',
// Shipping
'weight' => 'nullable|numeric|min:0',
'length' => 'nullable|numeric|min:0',
'width' => 'nullable|numeric|min:0',
'height' => 'nullable|numeric|min:0',
// Attributes
'attributes' => 'nullable|array',
'attributes.*.name' => 'required_with:attributes|string|max:100',
'attributes.*.value' => 'required_with:attributes|string|max:255',
];
}
public function messages()
{
return [
'name.required' => 'Product name is required',
'slug.unique' => 'This product slug is already in use',
'slug.regex' => 'Slug may only contain lowercase letters, numbers, and hyphens',
'price.required' => 'Product price is required',
'price.min' => 'Price must be at least :min',
'sale_price.lt' => 'Sale price must be less than regular price',
'sku.required' => 'SKU is required',
'sku.unique' => 'This SKU is already in use',
'category_id.required' => 'Please select a product category',
'category_id.exists' => 'The selected category is invalid',
'variants.required_if' => 'At least one variant is required when product has variants',
'variants.*.sku.unique' => 'Variant SKU must be unique',
'featured_image.dimensions' => 'Featured image must be at least 800x800 pixels',
];
}
public function attributes()
{
return [
'category_id' => 'category',
'variants.*.name' => 'variant name',
'variants.*.sku' => 'variant SKU',
'variants.*.price' => 'variant price',
'seo_title' => 'SEO title',
'seo_description' => 'SEO description',
];
}
protected function prepareForValidation()
{
// Generate slug from name if not provided
if (!$this->slug && $this->name) {
$this->merge([
'slug' => \Str::slug($this->name),
]);
}
// Set default values for booleans
$this->merge([
'backorder' => $this->boolean('backorder'),
'requires_shipping' => $this->boolean('requires_shipping'),
'has_variants' => $this->boolean('has_variants'),
]);
}
}
Custom Barcode Rule
// app/Rules/Barcode.php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class Barcode implements Rule
{
public function passes($attribute, $value)
{
// Skip validation if empty
if (empty($value)) {
return true;
}
// UPC-A: Must be 12 digits
if (strlen($value) === 12 && ctype_digit($value)) {
return $this->validateUpcA($value);
}
// EAN-13: Must be 13 digits
if (strlen($value) === 13 && ctype_digit($value)) {
return $this->validateEan13($value);
}
// Not a recognized barcode format
return false;
}
protected function validateUpcA($barcode)
{
// UPC-A check digit validation
$sum = 0;
for ($i = 0; $i < 11; $i++) {
$sum += $i % 2 === 0 ? $barcode[$i] * 3 : $barcode[$i];
}
$checkDigit = (10 - ($sum % 10)) % 10;
return $checkDigit === (int) $barcode[11];
}
protected function validateEan13($barcode)
{
// EAN-13 check digit validation
$sum = 0;
for ($i = 0; $i < 12; $i++) {
$sum += $i % 2 === 0 ? $barcode[$i] : $barcode[$i] * 3;
}
$checkDigit = (10 - ($sum % 10)) % 10;
return $checkDigit === (int) $barcode[12];
}
public function message()
{
return 'The :attribute must be a valid UPC-A (12 digits) or EAN-13 (13 digits) barcode.';
}
}
Controller Method
// app/Http/Controllers/ProductController.php
public function store(StoreProductRequest $request)
{
// All validation has passed at this point
$validated = $request->validated();
// Start database transaction
DB::beginTransaction();
try {
// Create the product
$product = new Product();
$product->name = $validated['name'];
$product->slug = $validated['slug'];
$product->description = $validated['description'];
$product->short_description = $validated['short_description'] ?? null;
$product->price = $validated['price'];
$product->sale_price = $validated['sale_price'] ?? null;
$product->cost = $validated['cost'] ?? null;
$product->sku = $validated['sku'];
$product->barcode = $validated['barcode'] ?? null;
$product->quantity = $validated['quantity'];
$product->backorder = $validated['backorder'];
$product->requires_shipping = $validated['requires_shipping'];
$product->category_id = $validated['category_id'];
$product->has_variants = $validated['has_variants'];
$product->weight = $validated['weight'] ?? null;
$product->length = $validated['length'] ?? null;
$product->width = $validated['width'] ?? null;
$product->height = $validated['height'] ?? null;
$product->seo_title = $validated['seo_title'] ?? null;
$product->seo_description = $validated['seo_description'] ?? null;
$product->seo_keywords = $validated['seo_keywords'] ?? null;
$product->save();
// Handle featured image
if ($request->hasFile('featured_image')) {
$path = $request->file('featured_image')->store('products', 'public');
$product->featured_image = $path;
$product->save();
}
// Handle gallery images
if ($request->hasFile('gallery')) {
foreach ($request->file('gallery') as $image) {
$path = $image->store('products/gallery', 'public');
$product->images()->create(['path' => $path]);
}
}
// Handle tags
if (isset($validated['tags'])) {
$product->tags()->sync($validated['tags']);
}
// Handle variants
if ($validated['has_variants'] && isset($validated['variants'])) {
foreach ($validated['variants'] as $variantData) {
$variant = new ProductVariant();
$variant->product_id = $product->id;
$variant->name = $variantData['name'];
$variant->sku = $variantData['sku'];
$variant->price = $variantData['price'];
$variant->quantity = $variantData['quantity'];
$variant->save();
}
}
// Handle attributes
if (isset($validated['attributes'])) {
foreach ($validated['attributes'] as $attributeData) {
$product->attributes()->create([
'name' => $attributeData['name'],
'value' => $attributeData['value'],
]);
}
}
DB::commit();
return redirect()->route('products.show', $product)
->with('success', 'Product created successfully!');
} catch (\Exception $e) {
DB::rollBack();
return back()->withInput()
->with('error', 'An error occurred while creating the product.');
}
}
This comprehensive example demonstrates:
- Complex validation rules for a real-world product form
- Custom rule objects for specialized validation (barcode)
- Conditional validation based on form inputs (variants)
- Handling file uploads with constraints
- Validation of nested arrays (variants, attributes)
- Database-related validation (unique, exists)
- Custom error messages and attribute names
- Input preprocessing with prepareForValidation()
- Using transactions to maintain data integrity
This pattern can be adapted for many complex forms in your applications.
Validation Best Practices
- Always Validate Server-Side - Never rely solely on client-side validation
- Use Form Requests for Complex Forms - Keeps controllers clean and validation reusable
- Create Custom Rules for Repeated Logic - Avoid duplicating complex validation patterns
- Be Specific with Error Messages - Clear, actionable messages help users correct their input
- Validate Early, Fail Fast - Validate input before performing any business logic
- Use Transactions for Related Operations - Ensure data integrity when validation passes but database operations could fail
- Consider Localization - Plan for translating validation messages in multi-language applications
- Test Validation Logic - Write unit tests to verify validation behavior
- Combine Server and Client Validation - For the best user experience and security
- Document Validation Requirements - Help API consumers understand what's required
- Consider Performance - Avoid heavy validation logic when simple rules will suffice
- Set Reasonable Limits - Define maximum lengths, file sizes, etc., to prevent abuse
Practice Activity
Custom Validation Rule Exercise
Create a custom validation rule for validating phone numbers that:
- Accepts common formats (e.g., +1 (555) 123-4567, 555-123-4567, 5551234567)
- Requires a valid country code when the international format is used
- Normalizes the phone number for storage (removing formatting characters)
- Provides helpful error messages specific to the validation failure
Conditional Validation Challenge
Create a registration form with conditional validation:
- Business accounts require company name, tax ID, and a business email domain
- Individual accounts require first name, last name, and date of birth
- Both account types need a valid email, password, and address
- If a referral code is provided, it must exist in the referral_codes table
- If signing up for the newsletter, a proper email is required
Advanced API Validation Exercise
Create an API endpoint for creating a new project with:
- Required fields: name, description, start_date
- Optional fields: end_date, budget, client_id, team_members
- Validation rules for all fields, including complex rules like:
- end_date must be after start_date
- budget must be a positive number with two decimal places
- team_members must be an array of existing user IDs
- Custom error response format following the JSON:API specification
- Rate limiting to prevent abuse
Summary
- Laravel provides a wide range of built-in validation rules for common scenarios
- Rules can be combined and customized to create sophisticated validation requirements
- Custom validation rules can be created using rule objects, closures, or extending the validator
- Validation error messages can be customized at multiple levels
- Manual validation provides more control over the validation process
- API validation can return JSON responses with validation errors
- JavaScript can be used alongside server-side validation for improved user experience
- Complex forms benefit from form requests, custom rules, and thoughtful organization
- Following validation best practices helps create robust, user-friendly applications
In the next lecture, we'll explore advanced validation techniques, form request composition, and integrating validation with authentication and authorization systems.