webgriffe/payum-lock-request-extension-bundle
Symfony bundle providing a Payum extension that locks payment requests, helping prevent duplicate or concurrent processing of the same payment. Integrates into Payum flows to guard request handling with a lock, reducing risks of double charges and race conditions.
Install
composer require webgriffe/payum-lock-request-extension-bundle
No Flex recipe is published, so register the bundle manually in config/bundles.php:
return [
// ...
Webgriffe\PayumLockRequestExtensionBundle\WebgriffePayumLockRequestExtensionBundle::class => ['all' => true],
];
Where to look first
vendor/webgriffe/payum-lock-request-extension-bundle/src/DependencyInjection/ — confirms the config key (typically webgriffe_payum_lock_request) and which services are auto-wired.Resources/config/services.xml — shows whether the extension is registered for all Payum gateways or must be attached per gateway.vendor/webgriffe/payum-lock-request-extension/src/ — the actual extension class. The bundle is a thin wrapper; the locking logic (key derivation, acquire/release, exception type) lives here and is what you will debug.Run bin/console config:dump-reference webgriffe_payum_lock_request to see the exact options for your installed version — the bundle is young enough that option names have moved.
First use case: prevent double capture
The classic failure this solves: a customer double-clicks "Pay", two CaptureAction executions race, and you either double-charge or throw a confusing Payum state exception. Point the extension at a Symfony Lock factory and it serialises execution per payment token.
# config/packages/payum_lock_request.yaml
webgriffe_payum_lock_request:
lock_factory: 'lock.factory' # any Symfony LockFactory; see below
# config/services.yaml
services:
lock.factory:
class: Symfony\Component\Lock\Store\RedisStore
arguments: ['@redis.payment_lock_connection']
Then capture the exception in your controller and return HTTP 409/redirect to the "processing" page:
try {
$gateway->execute(new Capture($token));
} catch (\Throwable $e) {
// lock already held -> show "payment in progress"
}
Lock on the payment token, not the cart
The extension keys the lock off the Payum token/payment details. This is usually correct: two different tokens for the same order can still race if your own code creates a second payment. If you need order-level protection, wrap your controller action in your own Symfony\Component\Lock\LockInterface keyed on the order ID in addition to this extension — do not try to override the extension's key derivation unless you fork the library.
Per-gateway vs global registration
Payum lets you register extensions globally (payum config) or per gateway (payum.gateways.<name>.extensions). Check which the bundle does. If it registers globally, every gateway (including your test offline gateway) now takes locks — usually fine, occasionally surprising in functional tests where two test cases share one Redis key.
Pair with status polling
This extension is not an idempotency layer. Use it together with a StatusAction/SyncAction flow: capture returns immediately, the client polls status, and the lock guarantees only one capture worker runs while the poller is free to read. Do not hold the lock across a long-running provider callback — locks have TTLs and an expired lock mid-capture is worse than no lock.
Symfony Lock store selection
flock — dev/single-host only.RedisStore / PdoStore::createTable() — required for multi-container production.Testing
Override lock_factory in config/packages/test/ with InMemoryStore. If you use Redis in tests, clear keys between tests — a leaked lock silently turns a passing test suite into a flaky one, and the failure looks like a random 409, not a lock.
webgriffe/payum-lock-request-extension. Read LockRequestExtension::execute() before filing an issue; the bundle just wires it.409 Conflict (or a redirect) in a kernel.exception listener, otherwise users see a 500 on double-click — the exact scenario the package exists to prevent.acquire() (non-blocking) on purpose; a blocking acquire(true) would queue a second capture behind the first and then execute it — the opposite of what you want. Don't "fix" it.release(), the lock expires on TTL, not immediately. Tune TTL down for fast PSPs, up for slow ones. Monitor for lock expiry warnings.config:dump-reference rather than copy-pasting from the README — the README is frequently the first thing to fall out of date in low-traffic bundles.payum/payum 1.x vs 2.x) your app uses; the extension hooks the gateway execute path, which changed between majors. Composer will usually resolve correctly, but pin explicitly.CaptureAction, or add a kernel.exception listener that inspects the lock exception and returns JSON for API callers vs an HTML flash for browser callers.How can I help you explore Laravel packages today?