Validation and Form Requests
Laravel provides a robust validation system that makes it easy to validate user input and handle validation errors. Form Requests allow you to encapsulate validation l
Laravel provides a robust validation system that makes it easy to validate user input and handle validation errors. Form Requests allow you to encapsulate validation logic in dedicated classes, keeping your controllers clean and maintainable.
Laravel's validation system provides:
Simple Controller Validation:
1<?php
2declare(strict_types=1);
3
4namespace App\Http\Controllers;
5
6use Illuminate\Http\Request;
7use Illuminate\Http\Response;
8use Illuminate\Support\Facades\Validator;
9
10class UserController extends Controller
11{
12 /**
13 * Store a new user (inline validation).
14 */
15 public function store(Request $request): Response
16 {
17 // Validate the request
18 $validated = $request->validate([
19 'name' => 'required|string|max:255',
20 'email' => 'required|email|unique:users,email',
21 'password' => 'required|string|min:8|confirmed',
22 'age' => 'nullable|integer|min:18|max:120',
23 'terms' => 'accepted',
24 ]);
25
26 // If validation passes, $validated contains the validated data
27 $user = User::create([
28 'name' => $validated['name'],
29 'email' => $validated['email'],
30 'password' => bcrypt($validated['password']),
31 'age' => $validated['age'] ?? null,
32 ]);
33
34 return response([
35 'message' => 'User created successfully',
36 'user' => $user,
37 ], 201);
38 }
39
40 /**
41 * Alternative: Manual validation with Validator facade.
42 */
43 public function storeManual(Request $request): Response
44 {
45 $validator = Validator::make($request->all(), [
46 'name' => 'required|string|max:255',
47 'email' => 'required|email|unique:users',
48 'password' => 'required|min:8|confirmed',
49 ]);
50
51 if ($validator->fails()) {
52 return response([
53 'message' => 'Validation failed',
54 'errors' => $validator->errors(),
55 ], 422);
56 }
57
58 $validated = $validator->validated();
59 $user = User::create($validated);
60
61 return response(['user' => $user], 201);
62 }
63}1<?php
2declare(strict_types=1);
3
4// Required and nullable
5'name' => 'required|string', // Must be present and string
6'nickname' => 'nullable|string', // Optional, but if present must be string
7
8// String length
9'username' => 'required|string|min:3|max:20',
10'bio' => 'nullable|string|max:500',
11'code' => 'required|string|size:6', // Exactly 6 characters
12
13// String format
14'email' => 'required|email',
15'url' => 'required|url',
16'ip' => 'required|ip',
17'uuid' => 'required|uuid',
18'slug' => 'required|alpha_dash', // Only letters, numbers, dashes, underscores
19'alpha' => 'required|alpha', // Only letters
20'alphanumeric' => 'required|alpha_num',
21
22// Regular expressions
23'phone' => 'required|regex:/^[0-9]{3}-[0-9]{3}-[0-9]{4}$/',
24'postal_code' => 'required|regex:/^[A-Z][0-9][A-Z] [0-9][A-Z][0-9]$/',1<?php
2declare(strict_types=1);
3
4// Basic numeric
5'age' => 'required|integer',
6'price' => 'required|numeric', // Integer or decimal
7'rating' => 'required|decimal:1,2', // 1-2 decimal places
8
9// Numeric ranges
10'quantity' => 'required|integer|min:1|max:100',
11'percentage' => 'required|numeric|between:0,100',
12
13// Specific values
14'status' => 'required|in:pending,active,completed',
15'priority' => 'required|not_in:0',
16
17// Boolean
18'is_active' => 'required|boolean', // true, false, 1, 0, "1", "0"
19'accepted' => 'accepted', // Must be yes, on, 1, or true1<?php
2declare(strict_types=1);
3
4// Date formats
5'birth_date' => 'required|date',
6'start_date' => 'required|date_format:Y-m-d',
7'event_time' => 'required|date_format:Y-m-d H:i:s',
8
9// Date comparisons
10'start_date' => 'required|date|after:yesterday',
11'end_date' => 'required|date|after:start_date',
12'deadline' => 'required|date|before:2025-12-31',
13'birth_date' => 'required|date|before_or_equal:today',
14
15// Age validation
16'birth_date' => [
17 'required',
18 'date',
19 'before:' . now()->subYears(18)->format('Y-m-d'), // Must be 18+
20],1<?php
2declare(strict_types=1);
3
4// Basic file validation
5'avatar' => 'required|file|max:2048', // Max 2MB
6
7// Image validation
8'photo' => 'required|image|mimes:jpeg,png,jpg,gif|max:5120',
9'logo' => 'required|image|dimensions:min_width=100,min_height=100,max_width=1000,max_height=1000',
10
11// Document validation
12'document' => 'required|file|mimes:pdf,doc,docx|max:10240', // Max 10MB
13
14// Multiple files
15'attachments.*' => 'required|file|mimes:pdf,jpg,png|max:5120',1<?php
2declare(strict_types=1);
3
4// Unique validation
5'email' => 'required|email|unique:users,email',
6'username' => 'required|string|unique:users,username,{$userId}', // Ignore current user
7
8// Exists validation
9'category_id' => 'required|integer|exists:categories,id',
10'user_id' => 'required|integer|exists:users,id,deleted_at,NULL', // Not soft-deleted
11
12// Unique with conditions
13'email' => [
14 'required',
15 'email',
16 Rule::unique('users')->where(function ($query) {
17 return $query->where('account_type', 'personal');
18 }),
19],Generate a custom rule:
1php artisan make:rule UppercaseImplement the rule:
1<?php
2declare(strict_types=1);
3
4namespace App\Rules;
5
6use Closure;
7use Illuminate\Contracts\Validation\ValidationRule;
8
9class Uppercase implements ValidationRule
10{
11 /**
12 * Run the validation rule.
13 */
14 public function validate(string $attribute, mixed $value, Closure $fail): void
15 {
16 if (strtoupper($value) !== $value) {
17 $fail("The {$attribute} must be uppercase.");
18 }
19 }
20}Using custom rules:
1<?php
2declare(strict_types=1);
3
4use App\Rules\Uppercase;
5
6$request->validate([
7 'country_code' => ['required', 'string', new Uppercase()],
8]);1<?php
2declare(strict_types=1);
3
4namespace App\Rules;
5
6use Closure;
7use Illuminate\Contracts\Validation\ValidationRule;
8
9class StrongPassword implements ValidationRule
10{
11 private int $minLength;
12 private bool $requireNumbers;
13 private bool $requireSpecialChars;
14
15 public function __construct(
16 int $minLength = 8,
17 bool $requireNumbers = true,
18 bool $requireSpecialChars = true
19 ) {
20 $this->minLength = $minLength;
21 $this->requireNumbers = $requireNumbers;
22 $this->requireSpecialChars = $requireSpecialChars;
23 }
24
25 public function validate(string $attribute, mixed $value, Closure $fail): void
26 {
27 if (!is_string($value)) {
28 $fail("The {$attribute} must be a string.");
29 return;
30 }
31
32 if (strlen($value) < $this->minLength) {
33 $fail("The {$attribute} must be at least {$this->minLength} characters.");
34 return;
35 }
36
37 if ($this->requireNumbers && !preg_match('/[0-9]/', $value)) {
38 $fail("The {$attribute} must contain at least one number.");
39 return;
40 }
41
42 if ($this->requireSpecialChars && !preg_match('/[^a-zA-Z0-9]/', $value)) {
43 $fail("The {$attribute} must contain at least one special character.");
44 return;
45 }
46
47 // Check for uppercase and lowercase
48 if (!preg_match('/[a-z]/', $value) || !preg_match('/[A-Z]/', $value)) {
49 $fail("The {$attribute} must contain both uppercase and lowercase letters.");
50 }
51 }
52}
53
54// Usage
55$request->validate([
56 'password' => [
57 'required',
58 new StrongPassword(minLength: 12, requireSpecialChars: true),
59 ],
60]);1<?php
2declare(strict_types=1);
3
4namespace App\Rules;
5
6use App\Models\User;
7use Closure;
8use Illuminate\Contracts\Validation\ValidationRule;
9
10class UniqueUsername implements ValidationRule
11{
12 private ?int $exceptUserId;
13
14 public function __construct(?int $exceptUserId = null)
15 {
16 $this->exceptUserId = $exceptUserId;
17 }
18
19 public function validate(string $attribute, mixed $value, Closure $fail): void
20 {
21 $query = User::where('username', $value);
22
23 if ($this->exceptUserId !== null) {
24 $query->where('id', '!=', $this->exceptUserId);
25 }
26
27 if ($query->exists()) {
28 $fail("The {$attribute} has already been taken.");
29 }
30 }
31}
32
33// Usage in update scenario
34$request->validate([
35 'username' => ['required', new UniqueUsername(auth()->id())],
36]);Generate a form request:
1php artisan make:request StoreUserRequestBasic Form Request:
1<?php
2declare(strict_types=1);
3
4namespace App\Http\Requests;
5
6use Illuminate\Foundation\Http\FormRequest;
7
8class StoreUserRequest extends FormRequest
9{
10 /**
11 * Determine if the user is authorized to make this request.
12 */
13 public function authorize(): bool
14 {
15 // Only authenticated users can create users
16 return $this->user() !== null;
17 }
18
19 /**
20 * Get the validation rules that apply to the request.
21 *
22 * @return array<string, array<int, string|\Illuminate\Contracts\Validation\ValidationRule>>
23 */
24 public function rules(): array
25 {
26 return [
27 'name' => ['required', 'string', 'max:255'],
28 'email' => ['required', 'email', 'unique:users,email'],
29 'password' => ['required', 'string', 'min:8', 'confirmed'],
30 'role' => ['required', 'string', 'in:user,admin,moderator'],
31 ];
32 }
33
34 /**
35 * Get custom messages for validator errors.
36 *
37 * @return array<string, string>
38 */
39 public function messages(): array
40 {
41 return [
42 'name.required' => 'Please provide your full name',
43 'email.required' => 'We need your email address',
44 'email.email' => 'Please provide a valid email address',
45 'email.unique' => 'This email is already registered',
46 'password.min' => 'Password must be at least :min characters',
47 'password.confirmed' => 'Password confirmation does not match',
48 'role.in' => 'Invalid role selected',
49 ];
50 }
51
52 /**
53 * Get custom attributes for validator errors.
54 *
55 * @return array<string, string>
56 */
57 public function attributes(): array
58 {
59 return [
60 'email' => 'email address',
61 'password' => 'password',
62 ];
63 }
64}Using Form Request in Controller:
1<?php
2declare(strict_types=1);
3
4namespace App\Http\Controllers;
5
6use App\Http\Requests\StoreUserRequest;
7use App\Models\User;
8use Illuminate\Http\Response;
9
10class UserController extends Controller
11{
12 /**
13 * Store a new user.
14 *
15 * Validation happens automatically before this method runs.
16 * If validation fails, a 422 response with errors is returned.
17 */
18 public function store(StoreUserRequest $request): Response
19 {
20 // $request->validated() only contains validated data
21 $user = User::create($request->validated());
22
23 return response(['user' => $user], 201);
24 }
25}1<?php
2declare(strict_types=1);
3
4namespace App\Http\Requests;
5
6use App\Models\Post;
7use App\Rules\StrongPassword;
8use Illuminate\Foundation\Http\FormRequest;
9use Illuminate\Validation\Rule;
10
11class UpdateUserRequest extends FormRequest
12{
13 /**
14 * Authorization with policy check.
15 */
16 public function authorize(): bool
17 {
18 $user = $this->route('user');
19 return $this->user()->can('update', $user);
20 }
21
22 /**
23 * Rules with conditional logic.
24 */
25 public function rules(): array
26 {
27 $userId = $this->route('user')->id;
28
29 return [
30 'name' => ['required', 'string', 'max:255'],
31 'email' => [
32 'required',
33 'email',
34 Rule::unique('users')->ignore($userId),
35 ],
36 'password' => [
37 'sometimes', // Only validate if present
38 'nullable',
39 new StrongPassword(),
40 'confirmed',
41 ],
42 'avatar' => [
43 'sometimes',
44 'image',
45 'mimes:jpeg,png,jpg',
46 'max:2048',
47 'dimensions:min_width=100,min_height=100',
48 ],
49 'bio' => ['nullable', 'string', 'max:1000'],
50 'settings' => ['sometimes', 'array'],
51 'settings.theme' => ['required_with:settings', 'in:light,dark'],
52 'settings.notifications' => ['sometimes', 'boolean'],
53 ];
54 }
55
56 /**
57 * Prepare data before validation.
58 */
59 protected function prepareForValidation(): void
60 {
61 $this->merge([
62 'slug' => str($this->name)->slug(),
63 'formatted_email' => strtolower($this->email),
64 ]);
65 }
66
67 /**
68 * Handle a passed validation attempt.
69 */
70 protected function passedValidation(): void
71 {
72 // Log successful validation
73 logger('User update validation passed', [
74 'user_id' => $this->route('user')->id,
75 'updated_fields' => array_keys($this->validated()),
76 ]);
77 }
78
79 /**
80 * Get validated data with additional processing.
81 */
82 public function validatedWithDefaults(): array
83 {
84 $validated = $this->validated();
85
86 // Add default values
87 $validated['settings'] = array_merge([
88 'theme' => 'light',
89 'notifications' => true,
90 'language' => 'en',
91 ], $validated['settings'] ?? []);
92
93 return $validated;
94 }
95}1<?php
2declare(strict_types=1);
3
4$request->validate([
5 // Only validate if present
6 'nickname' => 'sometimes|required|string|max:50',
7
8 // Required if another field is present
9 'other_country' => 'required_if:country,other',
10
11 // Required unless another field has certain value
12 'reason' => 'required_unless:status,active',
13
14 // Required with other fields
15 'city' => 'required_with:address,postal_code',
16
17 // Required with all other fields
18 'state' => 'required_with_all:city,postal_code,country',
19
20 // Required without other fields
21 'mobile' => 'required_without:phone,email',
22
23 // Required without all other fields
24 'contact_method' => 'required_without_all:phone,email,address',
25]);1<?php
2declare(strict_types=1);
3
4namespace App\Http\Requests;
5
6use Illuminate\Foundation\Http\FormRequest;
7use Illuminate\Validation\Rule;
8
9class StorePostRequest extends FormRequest
10{
11 public function rules(): array
12 {
13 $rules = [
14 'title' => ['required', 'string', 'max:255'],
15 'content' => ['required', 'string'],
16 'status' => ['required', 'in:draft,published'],
17 ];
18
19 // Add published_at validation only if status is published
20 if ($this->input('status') === 'published') {
21 $rules['published_at'] = ['required', 'date', 'after_or_equal:now'];
22 $rules['category_id'] = ['required', 'exists:categories,id'];
23 $rules['tags'] = ['required', 'array', 'min:1'];
24 $rules['tags.*'] = ['integer', 'exists:tags,id'];
25 }
26
27 // Add featured image validation if is_featured is true
28 if ($this->boolean('is_featured')) {
29 $rules['featured_image'] = ['required', 'image', 'max:5120'];
30 }
31
32 return $rules;
33 }
34
35 /**
36 * Configure the validator instance.
37 */
38 public function withValidator($validator): void
39 {
40 $validator->after(function ($validator) {
41 // Custom validation logic after all rules passed
42 if ($this->input('status') === 'published') {
43 $wordCount = str_word_count($this->input('content'));
44 if ($wordCount < 300) {
45 $validator->errors()->add(
46 'content',
47 'Published posts must have at least 300 words'
48 );
49 }
50 }
51 });
52 }
53}1<?php
2declare(strict_types=1);
3
4// Basic array validation
5$request->validate([
6 'tags' => 'required|array|min:1|max:5',
7 'tags.*' => 'integer|exists:tags,id',
8]);
9
10// Nested object validation
11$request->validate([
12 'user' => 'required|array',
13 'user.name' => 'required|string|max:255',
14 'user.email' => 'required|email',
15 'user.address' => 'required|array',
16 'user.address.street' => 'required|string',
17 'user.address.city' => 'required|string',
18 'user.address.postal_code' => 'required|string',
19]);
20
21// Multiple nested items
22$request->validate([
23 'products' => 'required|array|min:1',
24 'products.*.id' => 'required|integer|exists:products,id',
25 'products.*.quantity' => 'required|integer|min:1',
26 'products.*.price' => 'required|numeric|min:0',
27 'products.*.options' => 'sometimes|array',
28 'products.*.options.*.key' => 'required|string',
29 'products.*.options.*.value' => 'required|string',
30]);1<?php
2declare(strict_types=1);
3
4namespace App\Http\Requests;
5
6use Illuminate\Foundation\Http\FormRequest;
7
8class StoreOrderRequest extends FormRequest
9{
10 public function rules(): array
11 {
12 $rules = [
13 'customer_id' => ['required', 'integer', 'exists:customers,id'],
14 'items' => ['required', 'array', 'min:1'],
15 ];
16
17 // Dynamically add rules for each item
18 foreach ($this->input('items', []) as $index => $item) {
19 $rules["items.{$index}.product_id"] = [
20 'required',
21 'integer',
22 'exists:products,id',
23 ];
24 $rules["items.{$index}.quantity"] = [
25 'required',
26 'integer',
27 'min:1',
28 'max:' . ($item['max_quantity'] ?? 100),
29 ];
30 $rules["items.{$index}.price"] = [
31 'required',
32 'numeric',
33 'min:0',
34 ];
35 }
36
37 return $rules;
38 }
39}In Blade Templates:
1@if ($errors->any())
2 <div class="alert alert-danger">
3 <ul>
4 @foreach ($errors->all() as $error)
5 <li>{{ $error }}</li>
6 @endforeach
7 </ul>
8 </div>
9@endif
10
11{{-- Display error for specific field --}}
12@error('email')
13 <div class="text-red-500 text-sm mt-1">{{ $message }}</div>
14@enderror
15
16{{-- In form inputs --}}
17<input
18 type="email"
19 name="email"
20 value="{{ old('email') }}"
21 class="@error('email') border-red-500 @enderror"
22>In API Responses:
1<?php
2declare(strict_types=1);
3
4namespace App\Http\Controllers\API;
5
6use App\Http\Requests\StoreUserRequest;
7use App\Models\User;
8use Illuminate\Http\JsonResponse;
9
10class UserController extends Controller
11{
12 /**
13 * Store a new user via API.
14 */
15 public function store(StoreUserRequest $request): JsonResponse
16 {
17 $user = User::create($request->validated());
18
19 return response()->json([
20 'success' => true,
21 'message' => 'User created successfully',
22 'data' => $user,
23 ], 201);
24 }
25}
26
27// Failed validation automatically returns:
28// {
29// "message": "The given data was invalid.",
30// "errors": {
31// "email": ["The email has already been taken."],
32// "password": ["The password must be at least 8 characters."]
33// }
34// }1<?php
2declare(strict_types=1);
3
4namespace App\Http\Requests;
5
6use Illuminate\Foundation\Http\FormRequest;
7
8class ContactRequest extends FormRequest
9{
10 public function rules(): array
11 {
12 return [
13 'name' => ['required', 'string', 'max:100'],
14 'email' => ['required', 'email'],
15 'subject' => ['required', 'string', 'max:200'],
16 'message' => ['required', 'string', 'min:20', 'max:1000'],
17 'attachment' => ['sometimes', 'file', 'mimes:pdf,doc,docx', 'max:5120'],
18 ];
19 }
20
21 public function messages(): array
22 {
23 return [
24 'name.required' => 'What should we call you?',
25 'name.max' => 'Your name is too long (max :max characters)',
26 'email.required' => 'We need your email to respond',
27 'email.email' => 'That doesn't look like a valid email',
28 'subject.required' => 'Please tell us what this is about',
29 'message.required' => 'Please write your message',
30 'message.min' => 'Your message is too short (min :min characters)',
31 'message.max' => 'Your message is too long (max :max characters)',
32 'attachment.mimes' => 'Attachment must be PDF or Word document',
33 'attachment.max' => 'Attachment is too large (max :max KB)',
34 ];
35 }
36
37 public function attributes(): array
38 {
39 return [
40 'email' => 'email address',
41 'attachment' => 'file',
42 ];
43 }
44}1<?php
2declare(strict_types=1);
3
4// Organize by resource and action
5app/Http/Requests/
6 User/
7 StoreUserRequest.php
8 UpdateUserRequest.php
9 DeleteUserRequest.php
10 Post/
11 StorePostRequest.php
12 UpdatePostRequest.php
13 PublishPostRequest.php
14 Comment/
15 StoreCommentRequest.php
16 UpdateCommentRequest.php1<?php
2declare(strict_types=1);
3
4namespace App\Http\Requests\Concerns;
5
6trait ValidatesAddresses
7{
8 /**
9 * Get address validation rules.
10 *
11 * @return array<string, array<int, string>>
12 */
13 protected function addressRules(string $prefix = 'address'): array
14 {
15 return [
16 "{$prefix}.street" => ['required', 'string', 'max:255'],
17 "{$prefix}.city" => ['required', 'string', 'max:100'],
18 "{$prefix}.state" => ['required', 'string', 'size:2'],
19 "{$prefix}.postal_code" => ['required', 'string', 'regex:/^d{5}(-d{4})?$/'],
20 "{$prefix}.country" => ['required', 'string', 'size:2'],
21 ];
22 }
23}
24
25// Use in multiple requests
26class StoreOrderRequest extends FormRequest
27{
28 use ValidatesAddresses;
29
30 public function rules(): array
31 {
32 return array_merge([
33 'customer_id' => ['required', 'exists:customers,id'],
34 'items' => ['required', 'array', 'min:1'],
35 ], $this->addressRules('shipping_address'), $this->addressRules('billing_address'));
36 }
37}1<?php
2declare(strict_types=1);
3
4namespace App\Http\Requests;
5
6use Illuminate\Foundation\Http\FormRequest;
7
8class ProcessPaymentRequest extends FormRequest
9{
10 public function rules(): array
11 {
12 return [
13 'payment_method' => ['required', 'in:card,bank,wallet'],
14
15 // Only validate card details if payment method is card
16 'card_number' => ['required_if:payment_method,card', 'string', 'size:16'],
17 'card_expiry' => ['required_if:payment_method,card', 'string', 'regex:/^d{2}/d{2}$/'],
18 'card_cvv' => ['required_if:payment_method,card', 'string', 'size:3'],
19
20 // Only validate bank details if payment method is bank
21 'bank_account' => ['required_if:payment_method,bank', 'string'],
22 'routing_number' => ['required_if:payment_method,bank', 'string', 'size:9'],
23
24 // Only validate wallet if payment method is wallet
25 'wallet_id' => ['required_if:payment_method,wallet', 'string', 'exists:wallets,id'],
26 ];
27 }
28
29 /**
30 * Stop validation on first failure for security.
31 */
32 protected $stopOnFirstFailure = true;
33}1<?php
2declare(strict_types=1);
3
4namespace App\Http\Requests;
5
6use Illuminate\Foundation\Http\FormRequest;
7
8class StoreProductRequest extends FormRequest
9{
10 public function rules(): array
11 {
12 return [
13 'name' => ['required', 'string', 'max:255'],
14 'price' => ['required', 'numeric', 'min:0'],
15 'quantity' => ['required', 'integer', 'min:0'],
16 'is_active' => ['required', 'boolean'],
17 ];
18 }
19
20 /**
21 * Get validated data with proper types.
22 *
23 * @return array{
24 * name: string,
25 * price: float,
26 * quantity: int,
27 * is_active: bool
28 * }
29 */
30 public function validatedData(): array
31 {
32 $validated = $this->validated();
33
34 return [
35 'name' => (string) $validated['name'],
36 'price' => (float) $validated['price'],
37 'quantity' => (int) $validated['quantity'],
38 'is_active' => (bool) $validated['is_active'],
39 ];
40 }
41}1<?php
2declare(strict_types=1);
3
4namespace App\Http\Requests;
5
6use App\Models\Post;
7use App\Rules\StrongPassword;
8use Illuminate\Foundation\Http\FormRequest;
9use Illuminate\Support\Str;
10use Illuminate\Validation\Rule;
11
12class StorePostRequest extends FormRequest
13{
14 /**
15 * Authorize based on user permissions.
16 */
17 public function authorize(): bool
18 {
19 return $this->user()->can('create', Post::class);
20 }
21
22 /**
23 * Get validation rules.
24 */
25 public function rules(): array
26 {
27 $rules = [
28 'title' => [
29 'required',
30 'string',
31 'min:10',
32 'max:255',
33 Rule::unique('posts', 'title'),
34 ],
35 'slug' => [
36 'sometimes',
37 'string',
38 'max:255',
39 'alpha_dash',
40 Rule::unique('posts', 'slug'),
41 ],
42 'excerpt' => ['nullable', 'string', 'max:500'],
43 'content' => ['required', 'string', 'min:100'],
44 'status' => ['required', 'in:draft,published,scheduled'],
45 'category_id' => ['required', 'integer', 'exists:categories,id'],
46 'tags' => ['sometimes', 'array', 'max:10'],
47 'tags.*' => ['integer', 'exists:tags,id'],
48 'featured_image' => [
49 'sometimes',
50 'image',
51 'mimes:jpeg,png,jpg,webp',
52 'max:5120',
53 'dimensions:min_width=800,min_height=600',
54 ],
55 'meta_description' => ['nullable', 'string', 'max:160'],
56 'meta_keywords' => ['nullable', 'array', 'max:10'],
57 'meta_keywords.*' => ['string', 'max:50'],
58 ];
59
60 // Additional rules for published posts
61 if ($this->input('status') === 'published') {
62 $rules['published_at'] = ['required', 'date', 'before_or_equal:now'];
63 }
64
65 // Additional rules for scheduled posts
66 if ($this->input('status') === 'scheduled') {
67 $rules['published_at'] = ['required', 'date', 'after:now'];
68 }
69
70 return $rules;
71 }
72
73 /**
74 * Custom messages.
75 */
76 public function messages(): array
77 {
78 return [
79 'title.required' => 'Every post needs a compelling title',
80 'title.min' => 'Title is too short (min :min characters)',
81 'title.unique' => 'A post with this title already exists',
82 'content.required' => 'Post content cannot be empty',
83 'content.min' => 'Post content must be at least :min characters',
84 'category_id.required' => 'Please select a category',
85 'category_id.exists' => 'Selected category does not exist',
86 'tags.max' => 'Maximum :max tags allowed',
87 'featured_image.dimensions' => 'Image must be at least 800x600 pixels',
88 'meta_description.max' => 'Meta description too long (max :max characters)',
89 ];
90 }
91
92 /**
93 * Prepare data before validation.
94 */
95 protected function prepareForValidation(): void
96 {
97 // Auto-generate slug from title if not provided
98 if (!$this->has('slug') && $this->has('title')) {
99 $this->merge([
100 'slug' => Str::slug($this->title),
101 ]);
102 }
103
104 // Set published_at to now for published posts if not provided
105 if ($this->input('status') === 'published' && !$this->has('published_at')) {
106 $this->merge([
107 'published_at' => now()->toDateTimeString(),
108 ]);
109 }
110 }
111
112 /**
113 * Configure validator.
114 */
115 public function withValidator($validator): void
116 {
117 $validator->after(function ($validator) {
118 // Ensure featured image is provided for published posts
119 if ($this->input('status') === 'published' && !$this->hasFile('featured_image')) {
120 $existingPost = Post::where('slug', $this->slug)->first();
121 if (!$existingPost || !$existingPost->featured_image) {
122 $validator->errors()->add(
123 'featured_image',
124 'Published posts must have a featured image'
125 );
126 }
127 }
128
129 // Validate content word count for published posts
130 if ($this->input('status') === 'published') {
131 $wordCount = str_word_count(strip_tags($this->input('content')));
132 if ($wordCount < 300) {
133 $validator->errors()->add(
134 'content',
135 "Published posts must have at least 300 words (current: {$wordCount})"
136 );
137 }
138 }
139 });
140 }
141
142 /**
143 * Get validated data with defaults.
144 *
145 * @return array<string, mixed>
146 */
147 public function validatedWithDefaults(): array
148 {
149 $validated = $this->validated();
150
151 return array_merge([
152 'author_id' => $this->user()->id,
153 'published_at' => $this->input('status') === 'draft' ? null : now(),
154 'excerpt' => $validated['excerpt'] ?? Str::limit(strip_tags($validated['content']), 200),
155 'views_count' => 0,
156 ], $validated);
157 }
158}---
Laravel's validation system provides:
Key Best Practices:
In the next lesson, we'll explore Database Migrations and Seeders to manage your database schema and test data.
Your progress is saved in this browser, on this device only.