<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('coupons', function (Blueprint $table) {
$table->id();
$table->string('code')->unique();
$table->unsignedInteger('discount_cents');
$table->unsignedInteger('max_redemptions')->default(1);
$table->unsignedInteger('redeemed_count')->default(0);
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('coupons');
}
};
<?php
namespace App\Rules;
use App\Models\Coupon;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class CouponIsRedeemable implements ValidationRule
{
public ?Coupon $coupon = null;
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$this->coupon = Coupon::where('code', $value)->first();
if ($this->coupon === null) {
$fail('That coupon code does not exist.');
return;
}
if ($this->coupon->expires_at !== null && $this->coupon->expires_at->isPast()) {
$fail('This coupon has expired.');
return;
}
if ($this->coupon->redeemed_count >= $this->coupon->max_redemptions) {
$fail('This coupon has already been fully redeemed.');
}
}
}
<?php
namespace App\Http\Requests;
use App\Rules\CouponIsRedeemable;
use Illuminate\Foundation\Http\FormRequest;
class RedeemCouponRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'code' => ['required', 'string', 'max:64', new CouponIsRedeemable],
];
}
public function messages(): array
{
return [
'code.required' => 'Please enter a coupon code.',
];
}
}
<?php
namespace App\Http\Controllers;
use App\Http\Requests\RedeemCouponRequest;
use App\Models\Coupon;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
class CouponController extends Controller
{
public function redeem(RedeemCouponRequest $request): JsonResponse
{
$code = $request->validated('code');
$coupon = DB::transaction(function () use ($code) {
$coupon = Coupon::where('code', $code)->lockForUpdate()->first();
if ($coupon->redeemed_count >= $coupon->max_redemptions) {
abort(409, 'This coupon has already been fully redeemed.');
}
$coupon->increment('redeemed_count');
return $coupon;
});
return response()->json([
'message' => 'Coupon redeemed.',
'discount_cents' => $coupon->discount_cents,
'remaining' => $coupon->max_redemptions - $coupon->redeemed_count,
]);
}
}
This snippet shows how Laravel's custom rule objects encapsulate a non-trivial validation concern — checking that a coupon code both exists and is still redeemable — so the logic lives in one testable place instead of being smeared across controllers.
The create_coupons_table migration defines the durable shape the rule depends on. A code column is marked unique so codes are unambiguous, and redemption state is modeled with max_redemptions, a redeemed_count counter, and an expires_at timestamp. Splitting a hard limit from a running counter (rather than a single boolean flag) lets a coupon be redeemed several times while still being cheap to check.
The CouponIsRedeemable rule is an invokable rule implementing ValidationRule. Its validate method receives the attribute, value, and a $fail closure, and short-circuits with a specific message for each failure mode: the code not existing, being expired, or being fully consumed. Looking the row up once and stashing it on $this->coupon means the same query can be reused rather than repeated. Using distinct $fail messages is deliberate — a generic "invalid coupon" message frustrates users, whereas "this coupon has expired" is actionable. The rule reads state but does not mutate it; redemption belongs in a transaction after validation passes.
The RedeemCouponRequest form request wires the rule into the framework's validation lifecycle. In rules, the code field is required, normalized as a string, and handed a fresh new CouponIsRedeemable instance. Building the rule inside rules (not as a shared singleton) keeps each request's lookup isolated.
The CouponController shows the payoff: because validation already guaranteed a redeemable coupon, redeem can focus on the state change. It wraps the increment in a DB::transaction and uses lockForUpdate to serialize concurrent redemptions of the same code, closing the check-then-act race that pure validation cannot. This is the key trade-off to understand: the rule provides friendly, early feedback, but the authoritative guard against overselling a limited coupon is the row lock at write time. A developer reaches for this pattern whenever a field's validity depends on database state and clear per-case messaging matters.
Related snips
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
Share this code
Here's the card — post it anywhere.