A form request validates HTTP input before a controller processes it. Browser CSRF middleware checks a different boundary: whether a credentialed browser request carries the expected token. A policy decides whether the current reviewer may change this case. A database transaction then makes the state transition and audit record one unit. These checks cannot substitute for each other. A lost HTTP response after commit leaves the client uncertain; a stable operation ID and database uniqueness rule let a retry recover the committed outcome instead of repeating the approval. Deadlock retry can rerun the transaction closure, so external email and other irreversible effects do not belong inside it.
Laravel Form Requests, Transactions, and Approval Replay
Working case
Reviewer 47 submits approval for case 62 from a form. The server commits but the connection drops before the browser sees the response. The browser retries. Without a command key, the service writes a second audit event and sends another notice. In another path, reviewer 81 submits a valid form for the same case; validation succeeds, but the approval must still be denied. The repaired command has a bounded operation ID and expected revision, checks current assignment under a row lock, and stores the outcome under a unique key. The transaction commits the case transition and outbox intent together. A retry with the same key returns the original result.
Implementation boundary
<?php
use App\Http\Requests\ApprovePermitRequest;
use App\Models\PermitCase;
use App\Services\ApprovalService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
final class ApprovePermitController
{
public function __invoke(
ApprovePermitRequest $request,
PermitCase $permit,
ApprovalService $approvalService,
): JsonResponse {
$outcome = DB::transaction(
fn () => $approvalService->approveOnce(
$request->user()->id,
$permit->id,
$request->validated(),
),
attempts: 3,
);
return response()->json(['case_id' => $outcome->caseId, 'status' => $outcome->status]);
}
}Use the web route's CSRF middleware and include a token in the actual form template. Put syntactic field limits in an ApprovePermitRequest, including operation ID shape and revision range. The controller should obtain reviewer identity from the authenticated request, not the body. Delegate to a service that starts a transaction, locks the case where supported, checks current permission and revision, and writes the operation and outbox event under database constraints. A duplicate operation should return the stored result if its original command fields match; the same key with different fields is a conflict. If deadlock retry is enabled, ensure the closure has only transactional database effects and is safe to execute again.
Cost and boundaries
Row locking serializes conflicting approvals for one case and can create wait time on a hot permit. A uniqueness index and retained operation records consume storage proportional to the replay window, and the outbox adds a row per accepted state change. Form validation and CSRF checks are modest compared with database work but reject bad browser traffic before a lock is taken. Deadlock retries may repeat reads and writes, increasing tail latency; they do not guarantee success and should be bounded. Measure lock wait, transaction duration, duplicate-key conflicts, rejected CSRF requests, and the time between approval commit and durable notification acceptance.
Failure trace
Post the browser form without a CSRF token, then with a valid token but reviewer 81; neither may mutate case 62. Send an invalid operation ID and a stale revision. Race two approvals with the same ID and assert one case transition, one audit operation, and one outbox event. Commit an approval, discard its HTTP response, and replay the same ID to recover the prior result. Retry the same ID with a different reason and require a conflict. Force a database exception after changing status; the outbox and status must both roll back. Trigger a deadlock retry in a test and confirm no email is sent from inside the retried closure.
Verification
- CSRF and field validation stop malformed browser commands.
- The service owns current permission, uniqueness, and transaction state.
- A replay returns one committed outcome without a second event.
Practice drill
Implement a form request, approval controller, service, unique operation table, and notification outbox. Seed reviewer 47 and reviewer 81 with different assignments. Write expected responses for missing token, malformed command, forbidden case, stale revision, exact replay, and changed-payload replay before coding. Execute the approval through an explicit transaction and only build the public response after commit. Add a fault injection immediately before the outbox insert and another after commit but before response delivery. Use the stable command key to show which state can be recovered by retry and which requires operator inspection.
Decision note
Validate the browser command, authorize current state, and commit an idempotent database outcome before reporting success.
Common Mistakes
- Treating valid form fields as authorization.
- Sending email inside a deadlock-retried transaction closure.
- Using a client-generated reviewer ID as trusted identity.
Related lessons
Laravel Policy, Query, and Queue Boundaries; Laravel Route Binding, Policy, and Private Resource Scope; Laravel Eloquent Loading and Cursor Page Cost; Laravel After-Commit Jobs and Outbox Recovery; API Mutation and Failure Contracts; Django Forms, CSRF, Atomic Approval, and On-Commit Work.
Apply and check
Build Project: Laravel permit review workflow and review Web Development: Laravel policy, query, and queue quiz.
