phil / idempotency
Each operation run once per idempotency key: keys scoped to their caller, request fingerprints, the response kept and replayed, three states with an expiring in-progress one, and stores on a port, with adapters for Doctrine DBAL and the Symfony cache under a Symfony lock, for HTTP APIs, Messenger ha
Requires
- php: >=8.5
- psr/clock: ^1.0
- symfony/uid: ^8.2
Requires (Dev)
- doctrine/dbal: ^4.5
- phpunit/phpunit: ^13.4
- psr/cache: ^3.0
- symfony/cache: ^8.2
- symfony/clock: ^8.2
- symfony/filesystem: ^8.2
- symfony/lock: ^8.2
- symfony/messenger: ^8.2
- symfony/process: ^8.2
Suggests
- doctrine/dbal: To keep the keys in a table
- psr/cache: To keep the keys in a cache pool
- symfony/lock: To keep the keys in a cache pool, with an atomic reservation
- symfony/messenger: To handle a received message once per key
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-12 23:51:36 UTC
README
Each operation run once per idempotency key, whatever the retries.
phil/idempotency remembers the key of an intention — the Idempotency-Key header of an API request, the identifier
a message carries, the event of a payment webhook, the token of a form — and runs the operation once. A retry with
the same payload gets the first answer again; a key reused with another payload is refused; a retry that arrives
while the first run is still going is told to come back after the seconds left on its reservation. A run that fails
lets the same payload run again.
Each record is in one of three states: in progress, completed or failed. The in-progress state expires on its own, so that a process killed in the middle of a run never keeps its key. Only the SHA-256 fingerprint of the payload is kept, never the payload.
It is a plain PHP library. The stores are a port, with an adapter for Doctrine DBAL — one table, an atomic insert —
and one for any PSR-6 cache pool, made atomic by the Symfony Lock component. A Messenger middleware handles a
received message once per key. In a Symfony application, phil/api-contract-bundle brings #[Idempotent]
controllers on top of it.
Documentation
| Document | What it covers |
|---|---|
| Installation | Requirements and optional dependencies. |
| The rule | Keys, scopes, fingerprints, the three states and the four outcomes. |
| The guard | IdempotencyGuard: run(), begin(), complete(), fail(), and how to use them for an API, a webhook and a form. |
| Stores | The DBAL table and the cache pool under a lock: atomicity, expiry, purge. |
| Messenger | The middleware, its keys, and what it does with a duplicate. |
| Exceptions | What the library throws, and the HTTP answer of each. |
| Extension points | A store of your own, and the contract test it can extend. |
| Testing | What the suite pins, the concurrency test and the fuzz target. |
| Reference | Every public type. |
Quick start
use Phil\Idempotency\{IdempotencyGuard, IdempotencyKey, IdempotencyScope, PayloadFingerprint, StoredResponse}; use Phil\Idempotency\Store\DbalIdempotencyStore; $guard = new IdempotencyGuard(new DbalIdempotencyStore($connection, $clock), $clock, retention: 86_400); $run = $guard->run( IdempotencyScope::of('client:42'), IdempotencyKey::fromHeader($request->headers->get(IdempotencyKey::HEADER)), PayloadFingerprint::ofRequest($request->getMethod(), $request->getRequestUri(), $request->getContent()), static fn (): StoredResponse => StoredResponse::of(201, ['Location' => ['/orders/7']], $orders->place($request)), ); // $run->replayed: true when the stored answer was returned without running the operation.
Requirements
PHP 8.5 or later, psr/clock 1 and symfony/uid 8.2. The DBAL store needs doctrine/dbal 4.5; the cache store
psr/cache 3 and symfony/lock 8.2; the middleware symfony/messenger 8.2.
Contributing
Contributions are welcome on the PHIL monorepo: read the contribution guide and the code of conduct. Report a vulnerability privately, as the security policy explains, never in a public issue.
License
MIT, © Philippe Gamache, doing business as Le Gnome Archiviste; a Parler Haut Interagir Librement project. See LICENSE.