FileNonceStore
File-based {@see NonceStoreInterface}: a single JSON file mapping nonce => expiry, guarded by `flock()` across the whole read-prune-check-write cycle. Why `flock()` and not an atomic rename: consuming a nonce is a read-modify-write (read the current set, prune expired entries, check membership, write the updated set back). An atomic rename only makes the final file swap atomic — it does nothing to stop two concurrent callers from both reading the pre-write set and independently deciding the same nonce is still fresh, which is exactly the race this store exists to close. Holding `LOCK_EX` for the full read-modify-write cycle serializes it correctly. For a single small JSON file with the lab's request volume this is the smallest correct primitive; it would need revisiting (e.g. sharding, a real datastore) under real concurrency. Pruning happens inline on every {@see consume()} call rather than via a separate sweep process: entries whose `$expiresAt` is at or before the current time are dropped before the incoming nonce is checked and added, so the store cannot grow unbounded and never needs a cron job.
FileNonceStore::__construct()
public function __construct(string $path, ?Closure $clock = null):Parameters
| Name | Type | Description |
|---|---|---|
| $path | string | Path to the JSON file backing this store; its directory is created on first use if missing. |
| $clock | (\Closure(): int | null) | Injectable clock for pruning tests; defaults to `time()`. |
FileNonceStore::consume()
public function consume(string $nonce, int $expiresAt): boolAtomically checks whether `$nonce` has already been consumed and, if not, records it (to expire at `$expiresAt`) in the same locked critical section — returns `true` the first time a nonce is seen, `false` on every subsequent replay. Expired entries are pruned inline on every call, replayed or not.
Parameters
| Name | Type | Description |
|---|---|---|
| $nonce | string | |
| $expiresAt | int |