CoolCMS API

FormSubmissionGuard
in package

FinalYes

One-time submission tokens for forms that create a record ("add" forms), so a double click or two concurrent POSTs of the same form create one record, not two.

The form carries a random token issued when it is rendered (issueToken()). The first submit claims it by inserting a row into cms_system_form_submissions; the primary key makes the claim atomic across concurrent requests and processes. Neither the session nor Redis can do that here: sessions live in Redis without locking, so two requests of one session read the same snapshot and both would see the token as unused.

Lifecycle of a claimed token:

  • complete() after a successful save stores the created record id; a later submit of the same token gets SubmissionClaim::Done with that id and is redirected to the record instead of saving;
  • release() after a failed save deletes the row, so the corrected form can be sent again with the same token (it is still in the re-rendered hidden field);
  • a submit arriving while the first one still runs gets SubmissionClaim::InProgress; awaitOutcome() waits a few seconds for the first request to finish.

Fail-open, like CronLockManager: a database error (missing table before the migration ran, outage) must not block saving, so the claim then passes as SubmissionClaim::Claimed and the failure is logged. Portable SQL only (plain INSERT plus the unique violation, no INSERT IGNORE), so the unit test runs on SQLite.

Table of Contents

Constants

FIELD  : string = 'submissionToken'
Name of the hidden form field carrying the token.
PURGE_LIMIT  : int = 200
Upper bound of rows purged per claim, so a backlog never makes one submit slow.
RETENTION_SECONDS  : int = 86400
Rows older than this are purged on the next claim; a form left open longer gets a fresh token on reload.
STATE_DONE  : int = 1
STATE_PENDING  : int = 0
TABLE  : string = 'cms_system_form_submissions'
TOKEN_LENGTH  : int = 32

Properties

$connection  : Connection

Methods

__construct()  : mixed
awaitOutcome()  : SubmissionOutcome
Waits until the submit that claimed the token first finishes, polling the row.
claim()  : SubmissionOutcome
Claims the token for the current submit.
complete()  : void
Marks the token as used by a successful save of the record `$entityId` (null when the form has no record id).
issueToken()  : string
Returns a new random token for the hidden field of a freshly rendered form.
isWellFormed()  : bool
Tells whether the submitted value has the shape of a token this class issues (anything else is rejected).
release()  : void
Frees the token after a failed save, so the corrected form can be submitted again.
now()  : string
purgeExpired()  : void
read()  : SubmissionOutcome|null
Returns the state of a claimed token, or null when no row exists (never claimed, released or purged).
retryClaim()  : SubmissionOutcome
The row vanished between the failed insert and the read (released in between): one more attempt.

Constants

FIELD

Name of the hidden form field carrying the token.

public string FIELD = 'submissionToken'

PURGE_LIMIT

Upper bound of rows purged per claim, so a backlog never makes one submit slow.

private int PURGE_LIMIT = 200

RETENTION_SECONDS

Rows older than this are purged on the next claim; a form left open longer gets a fresh token on reload.

private int RETENTION_SECONDS = 86400

Properties

Methods

__construct()

public __construct(Connection $connection) : mixed
Parameters
$connection : Connection

awaitOutcome()

Waits until the submit that claimed the token first finishes, polling the row.

public awaitOutcome(string $token, string $form[, float $timeoutSeconds = 10.0 ][, int $pollMicroseconds = 200000 ]) : SubmissionOutcome

Returns SubmissionClaim::Done once it saved, SubmissionClaim::Claimed when it failed (the row was released and this call claimed it for the caller), or SubmissionClaim::InProgress after the timeout.

Parameters
$token : string
$form : string
$timeoutSeconds : float = 10.0
$pollMicroseconds : int = 200000
Return values
SubmissionOutcome

claim()

Claims the token for the current submit.

public claim(string $token, string $form) : SubmissionOutcome
Parameters
$token : string
$form : string

identifies the form (presenter and action) only for diagnostics; the token is unique on its own

Return values
SubmissionOutcome

complete()

Marks the token as used by a successful save of the record `$entityId` (null when the form has no record id).

public complete(string $token, int|null $entityId) : void
Parameters
$token : string
$entityId : int|null

issueToken()

Returns a new random token for the hidden field of a freshly rendered form.

public issueToken() : string
Return values
string

isWellFormed()

Tells whether the submitted value has the shape of a token this class issues (anything else is rejected).

public isWellFormed(mixed $token) : bool
Parameters
$token : mixed
Return values
bool

release()

Frees the token after a failed save, so the corrected form can be submitted again.

public release(string $token) : void
Parameters
$token : string

retryClaim()

The row vanished between the failed insert and the read (released in between): one more attempt.

private retryClaim(string $token, string $form) : SubmissionOutcome
Parameters
$token : string
$form : string
Return values
SubmissionOutcome
On this page

Search results