<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class Uppercase implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if ($value !== strtoupper($value)) {
$fail("The {$attribute} must be uppercase.");
}
}
}
<?php
namespace App\Rules;
use App\Models\Coupon;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class ValidCoupon implements ValidationRule
{
public function __construct(
private ?int $userId = null
) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$coupon = Coupon::where('code', $value)->first();
if (!$coupon) {
$fail('The coupon code is invalid.');
return;
}
if ($coupon->expires_at && $coupon->expires_at->isPast()) {
$fail('This coupon has expired.');
return;
}
if ($coupon->max_uses && $coupon->times_used >= $coupon->max_uses) {
$fail('This coupon has reached its usage limit.');
return;
}
if ($this->userId && $coupon->hasBeenUsedBy($this->userId)) {
$fail('You have already used this coupon.');
}
}
}
<?php
namespace App\Http\Requests;
use App\Rules\Uppercase;
use App\Rules\ValidCoupon;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class CheckoutRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => ['required', 'email', Rule::unique('users')->ignore($this->user())],
'coupon' => ['nullable', 'string', new ValidCoupon($this->user()?->id)],
'country_code' => ['required', 'string', 'size:2', new Uppercase()],
// Closure-based inline rule
'quantity' => [
'required',
'integer',
'min:1',
function (string $attribute, mixed $value, Closure $fail) {
$product = Product::find($this->product_id);
if ($product && $value > $product->stock) {
$fail("Only {$product->stock} items available in stock.");
}
},
],
// Nested validation
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => ['required', 'exists:products,id'],
'items.*.quantity' => ['required', 'integer', 'min:1'],
];
}
}
Custom validation rules encapsulate complex validation logic in reusable classes. I create rule classes implementing ValidationRule with a validate() method receiving the attribute, value, and fail closure. Rules access databases, call APIs, or perform intricate checks. The Rule::unique()->ignore() pattern excludes current models during updates. Closure-based validation provides inline custom rules. The sometimes() method conditionally validates. Nested array validation uses dot notation. Custom error messages use translation keys. I combine rules with | or arrays. The validation system is extensible—perfect for business-specific constraints like checking inventory, validating coupons, or verifying external data.
Related snips
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
{
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
Laravel mix/Vite for asset compilation
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Laravel database migrations for schema management
import axios from 'axios';
export type NormalizedErrors = {
fields: Record<string, string>;
formLevel: string | null;
};
Frontend: normalize and display server validation errors
Share this code
Here's the card — post it anywhere.