Laravel Session Management: A Quick Guide to Storing User Data
Manage user sessions and temporary data storage.
Form validation is crucial for ensuring data integrity and application security. Laravel provides a clean, simple way to validate incoming data with powerful, readable rules.
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
]);
// If validation passes, code continues here
User::create($validated);
return redirect()->route('users.index');
}
use Illuminate\Support\Facades\Validator;
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
]);
if ($validator->fails()) {
return redirect('register')
->withErrors($validator)
->withInput();
}
// Store the user...
User::create($validator->validated());
}
php artisan make:request StoreUserRequest
<?php
// app/Http/Requests/StoreUserRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest
{
public function authorize(): bool
{
return true; // Set authorization logic
}
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
];
}
}
// In controller
public function store(StoreUserRequest $request)
{
// Validation already passed!
User::create($request->validated());
return redirect()->route('users.index');
}
'field' => 'required|string|max:255',
'field' => 'nullable|string', // Optional field
'field' => 'sometimes|required|email', // Only validate if present
'name' => 'required|string|max:255',
'title' => 'required|string|min:5|max:100',
'slug' => 'required|alpha_dash|unique:posts,slug',
'description' => 'nullable|string|min:10|max:1000',
'bio' => 'string|between:10,5000', // Between min and max
'age' => 'required|integer|min:18|max:100',
'price' => 'required|numeric|min:0|max:10000',
'quantity' => 'integer|min:1',
'rating' => 'numeric|between:1,5', // Between 1 and 5
'discount' => 'nullable|numeric|min:0|max:100',
'email' => 'required|email|unique:users,email',
'website' => 'nullable|url|max:255',
'personal_url' => 'sometimes|url|active_url', // Must be active URL
'email_array' => 'required|array',
'email_array.*' => 'email', // Validate each array element
'birth_date' => 'required|date|before:today',
'start_date' => 'required|date|after:today',
'end_date' => 'required|date|after:start_date',
'published_at' => 'nullable|date|after_or_equal:today',
'event_date' => 'date_format:Y-m-d H:i:s', // Specific format
'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048', // 2MB
'document' => 'nullable|file|mimes:pdf,doc,docx|max:5120', // 5MB
'photos' => 'sometimes|array|max:5',
'photos.*' => 'image|mimes:jpeg,png,jpg|max:1024', // Each photo
'resume' => 'required|mimetypes:application/pdf,application/msword',
'tags' => 'required|array|min:1|max:10',
'tags.*' => 'string|distinct|min:2|max:50', // No duplicates
'sizes' => 'array',
'sizes.*' => 'in:small,medium,large,xl', // Must be in list
'preferences' => 'sometimes|array',
'preferences.color' => 'required_if:preferences,array|string',
'email' => 'required|email|unique:users,email,NULL,id,account_id,1',
'category_id' => 'required|exists:categories,id',
'user_id' => 'exists:users,id,status,active', // User must exist and be active
'slug' => 'unique:posts,slug,' . $postId, // Ignore current post
public function rules(): array
{
return [
'first_name' => 'required|string|max:50|alpha',
'last_name' => 'required|string|max:50|alpha',
'username' => 'required|string|alpha_dash|min:3|max:30|unique:users',
'email' => 'required|email:rfc,dns|max:255|unique:users',
'password' => [
'required',
'string',
'min:8',
'confirmed',
'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/', // Strong password
],
'phone' => 'nullable|string|regex:/^\+?[1-9]\d{1,14}$/', // E.164 format
'birth_date' => 'required|date|before:-18 years', // Must be 18+ years old
'agree_terms' => 'required|accepted', // Checkbox must be checked
'newsletter' => 'sometimes|boolean',
];
}
public function rules(): array
{
return [
'name' => 'required|string|max:255|unique:products,name',
'slug' => 'required|string|alpha_dash|max:255|unique:products,slug',
'description' => 'required|string|min:50|max:2000',
'short_description' => 'required|string|max:300',
'price' => 'required|numeric|min:0|max:999999.99',
'sale_price' => 'nullable|numeric|lt:price', // Must be less than regular price
'sku' => 'required|string|max:100|unique:products,sku',
'stock_quantity' => 'required|integer|min:0|max:10000',
'weight' => 'nullable|numeric|min:0',
'dimensions' => 'nullable|array',
'dimensions.length' => 'required_with:dimensions|numeric|min:0',
'dimensions.width' => 'required_with:dimensions|numeric|min:0',
'dimensions.height' => 'required_with:dimensions|numeric|min:0',
'category_id' => 'required|exists:categories,id',
'brand_id' => 'nullable|exists:brands,id',
'tags' => 'sometimes|array|max:10',
'tags.*' => 'string|max:50',
'images' => 'sometimes|array|max:5',
'images.*' => 'image|mimes:jpeg,png,jpg,webp|max:5120',
'is_featured' => 'sometimes|boolean',
'is_active' => 'sometimes|boolean',
'meta_title' => 'nullable|string|max:60',
'meta_description' => 'nullable|string|max:160',
];
}
public function rules(): array
{
return [
'title' => 'required|string|min:10|max:255',
'slug' => 'required|string|alpha_dash|max:255|unique:posts,slug,' . $this->post?->id,
'content' => 'required|string|min:100|max:10000',
'excerpt' => 'required|string|max:500',
'featured_image' => 'sometimes|image|mimes:jpeg,png,jpg,webp|max:2048',
'category_id' => 'required|exists:categories,id',
'tags' => 'required|array|min:1|max:5',
'tags.*' => 'exists:tags,id',
'is_published' => 'sometimes|boolean',
'published_at' => 'required_if:is_published,true|nullable|date|after_or_equal:now',
'meta_title' => 'nullable|string|max:60',
'meta_description' => 'nullable|string|max:160',
'reading_time' => 'sometimes|integer|min:1|max:60',
];
}
php artisan make:rule StrongPassword
<?php
// app/Rules/StrongPassword.php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class StrongPassword implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
// Check minimum length
if (strlen($value) < 8) {
$fail('The :attribute must be at least 8 characters.');
return;
}
// Check for at least one uppercase letter
if (!preg_match('/[A-Z]/', $value)) {
$fail('The :attribute must contain at least one uppercase letter.');
return;
}
// Check for at least one lowercase letter
if (!preg_match('/[a-z]/', $value)) {
$fail('The :attribute must contain at least one lowercase letter.');
return;
}
// Check for at least one number
if (!preg_match('/[0-9]/', $value)) {
$fail('The :attribute must contain at least one number.');
return;
}
// Check for at least one special character
if (!preg_match('/[@$!%*#?&]/', $value)) {
$fail('The :attribute must contain at least one special character (@$!%*#?&).');
}
}
}
// Usage in form request
public function rules(): array
{
return [
'password' => ['required', 'confirmed', new StrongPassword],
];
}
<?php
// app/Rules/FileType.php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class FileType implements ValidationRule
{
protected $types;
public function __construct(array $types)
{
$this->types = $types;
}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$extension = strtolower($value->getClientOriginalExtension());
if (!in_array($extension, $this->types)) {
$fail('The :attribute must be a file of type: ' . implode(', ', $this->types));
}
}
}
// Usage
public function rules(): array
{
return [
'document' => ['required', 'file', new FileType(['pdf', 'doc', 'docx'])],
];
}
use Illuminate\Support\Facades\Validator;
$validator = Validator::make($request->all(), [
'start_date' => 'required|date',
'end_date' => 'required|date',
]);
$validator->after(function ($validator) use ($request) {
if ($request->start_date > $request->end_date) {
$validator->errors()->add(
'end_date',
'End date must be after start date.'
);
}
if (Carbon::parse($request->start_date)->diffInDays($request->end_date) > 30) {
$validator->errors()->add(
'end_date',
'The booking period cannot exceed 30 days.'
);
}
});
if ($validator->fails()) {
// Handle validation failure
}
public function messages(): array
{
return [
'name.required' => 'The full name field is required.',
'email.required' => 'We need your email address to create your account.',
'email.unique' => 'This email address is already registered. Please try logging in.',
'password.required' => 'Please choose a secure password for your account.',
'password.min' => 'Your password should be at least 8 characters long.',
'password.confirmed' => 'The password confirmation does not match.',
'agree_terms.accepted' => 'You must accept the terms and conditions to continue.',
'birth_date.before' => 'You must be at least 18 years old to register.',
// Array field messages
'tags.*.string' => 'Each tag must be a text value.',
'tags.*.distinct' => 'Duplicate tags are not allowed.',
'images.*.image' => 'Each file must be a valid image.',
'images.*.max' => 'Each image must not be larger than 5MB.',
];
}
public function attributes(): array
{
return [
'first_name' => 'first name',
'last_name' => 'last name',
'email' => 'email address',
'agree_terms' => 'terms and conditions',
'birth_date' => 'date of birth',
'category_id' => 'category',
'tags.*' => 'tag',
'images.*' => 'image',
];
}
<?php
// app/Http/Requests/UpdateProfileRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateProfileRequest extends FormRequest
{
public function authorize(): bool
{
// Only allow users to update their own profile
return auth()->check() && auth()->id() == $this->user->id;
}
public function rules(): array
{
$userId = auth()->id();
return [
'name' => 'required|string|max:255',
'email' => [
'required',
'email',
Rule::unique('users')->ignore($userId),
],
'username' => [
'required',
'alpha_dash',
'min:3',
'max:30',
Rule::unique('users')->ignore($userId),
],
'phone' => 'nullable|string|regex:/^\+?[1-9]\d{1,14}$/',
'bio' => 'nullable|string|max:1000',
'website' => 'nullable|url|max:255',
'location' => 'nullable|string|max:100',
'avatar' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
'social_links' => 'sometimes|array',
'social_links.facebook' => 'nullable|url|starts_with:https://facebook.com,https://www.facebook.com',
'social_links.twitter' => 'nullable|url|starts_with:https://twitter.com,https://x.com',
'social_links.linkedin' => 'nullable|url|starts_with:https://linkedin.com,https://www.linkedin.com',
'notification_settings' => 'sometimes|array',
'notification_settings.email' => 'boolean',
'notification_settings.push' => 'boolean',
'notification_settings.sms' => 'boolean',
];
}
public function messages(): array
{
return [
'email.unique' => 'This email address is already taken. Please use a different one.',
'username.unique' => 'This username is not available. Please choose another one.',
'phone.regex' => 'Please enter a valid phone number in international format.',
'avatar.max' => 'The profile picture must not be larger than 2MB.',
'social_links.facebook.starts_with' => 'Please enter a valid Facebook profile URL.',
'social_links.twitter.starts_with' => 'Please enter a valid Twitter profile URL.',
'social_links.linkedin.starts_with' => 'Please enter a valid LinkedIn profile URL.',
];
}
public function attributes(): array
{
return [
'social_links.facebook' => 'Facebook URL',
'social_links.twitter' => 'Twitter URL',
'social_links.linkedin' => 'LinkedIn URL',
'notification_settings.email' => 'email notifications',
'notification_settings.push' => 'push notifications',
'notification_settings.sms' => 'SMS notifications',
];
}
public function withValidator($validator)
{
$validator->after(function ($validator) {
if ($this->hasFile('avatar') && !$this->file('avatar')->isValid()) {
$validator->errors()->add('avatar', 'The avatar file upload failed. Please try again.');
}
});
}
}
public function rules(): array
{
return [
'payment_method' => 'required|in:credit_card,paypal,bank_transfer',
// Credit card validation (only if payment_method is credit_card)
'card_number' => 'required_if:payment_method,credit_card|string|size:16',
'expiry_date' => 'required_if:payment_method,credit_card|date_format:m/y|after:today',
'cvv' => 'required_if:payment_method,credit_card|string|size:3',
'card_holder' => 'required_if:payment_method,credit_card|string|max:255',
// PayPal validation
'paypal_email' => 'required_if:payment_method,paypal|email',
// Bank transfer validation
'account_holder' => 'required_if:payment_method,bank_transfer|string|max:255',
'account_number' => 'required_if:payment_method,bank_transfer|string|size:8',
'sort_code' => 'required_if:payment_method,bank_transfer|string|size:6',
// Billing address (always required)
'billing_address' => 'required|array',
'billing_address.street' => 'required|string|max:255',
'billing_address.city' => 'required|string|max:100',
'billing_address.state' => 'required|string|max:100',
'billing_address.zip_code' => 'required|string|max:20',
'billing_address.country' => 'required|string|max:100',
// Shipping address (only required if different from billing)
'shipping_same_as_billing' => 'sometimes|boolean',
'shipping_address' => 'required_unless:shipping_same_as_billing,true|array',
'shipping_address.street' => 'required_with:shipping_address|string|max:255',
'shipping_address.city' => 'required_with:shipping_address|string|max:100',
'shipping_address.state' => 'required_with:shipping_address|string|max:100',
'shipping_address.zip_code' => 'required_with:shipping_address|string|max:20',
'shipping_address.country' => 'required_with:shipping_address|string|max:100',
];
}
public function rules(): array
{
$rules = [
'type' => 'required|in:individual,company',
'name' => 'required|string|max:255',
'email' => 'required|email|unique:customers,email',
];
// Add conditional rules based on customer type
if ($this->type === 'individual') {
$rules['date_of_birth'] = 'required|date|before:-18 years';
$rules['personal_id'] = 'required|string|max:50';
} elseif ($this->type === 'company') {
$rules['company_name'] = 'required|string|max:255';
$rules['tax_id'] = 'required|string|max:50';
$rules['registration_number'] = 'required|string|max:100';
}
// Add phone validation if phone is provided
if ($this->has('phone')) {
$rules['phone'] = 'string|regex:/^\+?[1-9]\d{1,14}$/';
}
return $rules;
}
<form method="POST" action="{{ route('users.store') }}">
@csrf
<!-- Name Field -->
<div class="form-group">
<label for="name">Full Name</label>
<input
type="text"
name="name"
id="name"
value="{{ old('name') }}"
class="form-control @error('name') is-invalid @enderror"
required
>
@error('name')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<!-- Email Field -->
<div class="form-group">
<label for="email">Email Address</label>
<input
type="email"
name="email"
id="email"
value="{{ old('email') }}"
class="form-control @error('email') is-invalid @enderror"
required
>
@error('email')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<!-- Password Field -->
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
name="password"
id="password"
class="form-control @error('password') is-invalid @enderror"
required
>
@error('password')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<!-- Password Confirmation -->
<div class="form-group">
<label for="password_confirmation">Confirm Password</label>
<input
type="password"
name="password_confirmation"
id="password_confirmation"
class="form-control"
required
>
</div>
<!-- Array Fields (Tags) -->
<div class="form-group">
<label for="tags">Tags</label>
@for($i = 0; $i < 3; $i++)
<input
type="text"
name="tags[]"
value="{{ old('tags.' . $i) }}"
class="form-control mb-2 @error('tags.' . $i) is-invalid @enderror"
placeholder="Tag {{ $i + 1 }}"
>
@error('tags.' . $i)
<div class="invalid-feedback d-block">
{{ $message }}
</div>
@enderror
@endfor
@error('tags')
<div class="invalid-feedback d-block">
{{ $message }}
</div>
@enderror
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
<!-- Display all errors at the top -->
@if($errors->any())
<div class="alert alert-danger">
<h5>Please fix the following errors:</h5>
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
public function store(StoreUserRequest $request)
{
// For API, Laravel automatically returns JSON validation errors
$user = User::create($request->validated());
return response()->json([
'message' => 'User created successfully',
'user' => $user
], 201);
}
// Custom API validation response
public function failedValidation(Validator $validator)
{
throw new HttpResponseException(response()->json([
'success' => false,
'message' => 'Validation errors',
'errors' => $validator->errors()
], 422));
}
<?php
// app/Http/Requests/ApiRequest.php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
abstract class ApiRequest extends FormRequest
{
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response()->json([
'success' => false,
'message' => 'Validation failed',
'errors' => $validator->errors()
], 422));
}
protected function failedAuthorization()
{
throw new HttpResponseException(response()->json([
'success' => false,
'message' => 'This action is unauthorized.'
], 403));
}
}
// Use in specific API requests
class StoreUserRequest extends ApiRequest
{
// Your rules here...
}
<?php
// tests/Feature/UserRegistrationTest.php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserRegistrationTest extends TestCase
{
use RefreshDatabase;
public function test_user_registration_validation()
{
// Test required fields
$response = $this->post('/register', []);
$response->assertSessionHasErrors([
'name', 'email', 'password'
]);
// Test email validation
$response = $this->post('/register', [
'name' => 'John Doe',
'email' => 'invalid-email',
'password' => 'password',
'password_confirmation' => 'password',
]);
$response->assertSessionHasErrors('email');
// Test password confirmation
$response = $this->post('/register', [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password',
'password_confirmation' => 'different',
]);
$response->assertSessionHasErrors('password');
// Test unique email
$user = \App\Models\User::factory()->create(['email' => 'existing@example.com']);
$response = $this->post('/register', [
'name' => 'Jane Doe',
'email' => 'existing@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
]);
$response->assertSessionHasErrors('email');
}
public function test_successful_user_registration()
{
$response = $this->post('/register', [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'Password123!',
'password_confirmation' => 'Password123!',
'agree_terms' => true,
]);
$response->assertRedirect('/dashboard');
$this->assertDatabaseHas('users', [
'email' => 'john@example.com'
]);
}
}
$request->validate() is a convenient helper that automatically redirects back with errors, while Validator::make() gives you more control and returns a validator instance that you can check manually.
Use php artisan make:rule RuleName to generate a custom rule class, then implement the validate method with your custom logic. You can also use closure-based rules with Validator::make().
Form Request classes centralize validation logic, handle authorization, and can customize error messages and responses. They keep controllers clean and make validation reusable.
Use required_if, required_unless, required_with, or the sometimes rule. You can also use conditional logic in form requests to dynamically build rules arrays.
unique checks that a value doesn't already exist in the database, while exists checks that a value does exist in the database. Both are used for database integrity.
Use dot notation: 'tags.*' => 'required|string' validates each element in the tags array. You can also nest deeper: 'users.*.email' => 'required|email'.
// Basic
'required', 'nullable', 'sometimes'
// Strings
'string', 'email', 'url', 'ip', 'alpha', 'alpha_dash', 'alpha_num'
// Numbers
'integer', 'numeric', 'min:value', 'max:value', 'between:min,max'
// Dates
'date', 'date_format:format', 'before:date', 'after:date'
// Files
'file', 'image', 'mimes:jpeg,png', 'mimetypes:image/jpeg', 'max:size'
// Database
'exists:table,column', 'unique:table,column,except,idColumn'
// Arrays
'array', 'size:count', 'min:count', 'max:count'
// Conditions
'required_if:field,value', 'required_unless:field,value', 'required_with:field'
You've now mastered Laravel form validation and can ensure data integrity across your applications! In our next post, we'll explore Laravel Authentication Scaffolding: A Deep Dive into Laravel Breeze & Jetstream to learn about Laravel's authentication systems.