<?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'],
];
}
}