payum/omnipay-bridge
Bridge for Payum that lets you use any Omnipay payment gateway through Payum's unified API. Wrap Omnipay gateways as Payum payment services to handle purchases, authorizations, captures, refunds and notifications via one consistent interface.
What it actually is. A thin adapter that exposes any Omnipay\Common\GatewayInterface implementation as a Payum gateway factory named omnipay. Nothing more. Omnipay stays Omnipay; Payum stays Payum; this package only translates Capture/Authorize-style requests into Omnipay purchase()/authorize() calls and wraps the response.
Install (pin everything — this package last shipped in Jan 2017 and targets Payum 1.x):
composer require payum/payum:^1.5 payum/omnipay-bridge:^1.0 league/omnipay:^3.0
# plus the specific Omnipay driver you need, e.g.
composer require omnipay/stripe:^3.0
Where to look first. Read two classes before writing any code: Payum\OmnipayBridge\OmnipayGatewayFactory (see createGateway() and which config keys it reads) and the bridge gateway class it builds (Bridge/OmnipayGateway.php or similar). Those two files are the documentation — the README is a stub. Note which Payum actions are handled; the mapping is deliberately minimal.
Register the gateway. In config/payum.php (publish with php artisan vendor:publish --provider="Payum\LaravelPackage\PayumServiceProvider"), add to gateways:
'gateways' => [
'stripe' => [
'factory' => 'omnipay',
'payum.omnipay.gateway' => \Omnipay\Stripe\Gateway::class,
'payum.omnipay.gateway.parameters' => [
'apiKey' => env('STRIPE_SECRET'),
],
],
],
payum.omnipay.gateway is the Omnipay gateway class; payum.omnipay.gateway.parameters is handed to Omnipay's initialize() verbatim (so use the keys that specific driver expects — apiKey, testMode, username/password for SagePay, etc.).
First use case — an on-site purchase. The canonical Payum flow, which the bridge slots into without special casing:
// prepare
$payment = $storage->create();
$payment['amount'] = 1000; // verify units — see Gotchas
$payment['currency'] = 'EUR';
$storage->update($payment);
$token = $gateway->getTokenFactory()->createCaptureToken('stripe', $payment, 'payment.done');
return redirect($token->getTargetUrl());
// return route
$status = $gateway->execute(new GetHumanStatus($request->get('payum_token')));
if ($status->isCaptured()) { /* fulfil order */ }
That's the whole first milestone: config a gateway, create a token, let Payum's capture action drive the bridge, read status back.
Let Payum's token machinery carry the redirect. Off-site gateways (PayPal Express, SagePay, WorldPay) are the reason to use the bridge at all. The bridge returns the Omnipay redirect response; Payum's capture action converts that into an HTTP redirect and the token round-trips you back. Use createCaptureToken($gateway, $payment, 'return.route') and never manually stash state in the session — the token is the state. On return, always re-execute with the token rather than trusting query params.
Service-layer wrapper, one per gateway. Don't sprinkle $gateway->execute(...) through controllers. Register gateway names in config and inject Payum / GatewayRegistry into a small PaymentService that exposes charge(int $cents, string $currency, array $meta): Payment and handleReturn(Request): PaymentStatus. That keeps the bridge confined to one place — which matters because it will be the piece you replace.
Stateless vs stored models. For on-site gateways (Stripe, AuthorizeNet AIM) you have two options: pass a PaymentInterface object (ArrayObject-backed, zero storage config) or persist it and use a token. Persisted models give you replayable history and work uniformly for both gateway types; stateless is fine for pure API-only charges where you never redirect. Pick one and apply it across gateways so your controller code doesn't fork.
Integrating callbacks/webhooks. The bridge maps outbound requests, not inbound notifications. Omnipay's completePurchase() / IPN parsing expects the raw return payload, and the Payum request objects don't carry it. Practical answers: (a) use Payum's Notify action if the gateway driver supports it, or (b) pull the Omnipay gateway out of the container yourself ($gateway->getOmnipayGateway() if exposed, or instantiate the driver directly) and call completePurchase() in a webhook controller, bypassing the bridge. Don't fight the abstraction — for webhooks, going around the bridge is normal.
Queue the webhook route. Exempt it from CSRF (VerifyCsrfToken::$except), make it idempotent with a unique index on the transaction reference, and process inside a job. The bridge's 2017-era Omnipay dependency has no retry semantics of its own.
Testing. The bridge itself is awkward to mock — Payum's GatewayMock won't speak Omnipay. Instead: unit-test your service layer against a hand-rolled fake gateway; integration-test the bridge against an Omnipay gateway's test mode (Stripe test keys, PayPal sandbox); and use Omnipay's Http\Client\Common\Plugin\LoggerPlugin (see below) to assert the outgoing request shape.
Migration path / extension points. If you need behaviour the bridge lacks (partial refunds, void, payment-method tokenisation, 3DS), write a custom Payum action and register it on the gateway rather than subclassing the bridge. addGatewayFactory() in PayumBuilder lets you decorate or replace the factory wholesale — a useful escape hatch when you want to keep Payum's token/storage layer but stop using the generic mapping.
Maintenance risk is the headline gotcha. Last release January 2017, 23 stars, zero dependents. It targets Payum 1.x and Omnipay 2.x/3.x-era APIs. It is very unlikely to be compatible with Payum 2.x or later Omnipay/PHP 8. Treat it as a stopgap: budget for either pinning payum/payum to ^1.5 and freezing the dependency tree, or writing a ~150-line custom Payum gateway/action that speaks Omnipay directly (which is essentially what this package does — the code is small enough to vendor).
Amount units. This is the #1 silent bug. Omnipay drivers take a decimal string ('10.00'), Payum models conventionally carry integer minor units (1000). The bridge's conversion (if any) is undocumented. Verify with a log or a tiny test charge before going live, and centralise the conversion in one helper so you can flip it.
Config keys are passed straight through. payum.omnipay.gateway.parameters goes into Gateway::initialize() untouched. A typo like api_key vs apiKey produces a gateway with no credentials and a confusing remote 401, not a config error. Cross-check against the driver's own README.
Exceptions escape differently. Omnipay throws Omnipay\Common\Exception\* (InvalidResponseException, GatewayException) and returns failed ResponseInterface objects without throwing. Payum expects a Reply\*/RequestNotSupported vocabulary. Catch \Throwable around the execute() call and normalise into your own exception — do not assume $status->isCaptured() is false rather than an exception being thrown.
Debugging traffic. You get almost no visibility through Payum. Attach a logger to the Omnipay gateway's HTTP client before it's wrapped:
$gateway->setHttpClient(new \Omnipay\Common\Http\Client(
\Http\Discovery\Psr18ClientDiscovery::find() // or Guzzle with LoggerPlugin + MessageFormatter
));
then tail storage/logs/laravel.log. Also dump $response->getMessage() / getTransactionReference() from the Omnipay response — the bridge may swallow the useful detail.
Off-site return URLs must be absolute and route-addressable. Payum generates the target URL from route(); behind a proxy or with a mismatched APP_URL the gateway will bounce the user to the wrong host. Set APP_URL, TrustProxies, and URL::forceScheme('https') before debugging anything else.
Storage is mandatory for redirect gateways. If you only registered gateways and skipped storages / security.token_storage, token creation fails at runtime with a Payum storage error, not a helpful "you forgot config" message. Publish and fill in the full payum.php.
No tokenisation, no idempotency keys, no webhook verification. All of those are driver-level Omnipay concerns. If your processor supports idempotency keys, set them via the Omnipay parameters array — the bridge won't add them for you.
How can I help you explore Laravel packages today?