xola/omnipay-bundle
A Symfony bundle that integrates the Omnipay payment library, providing configurable gateway services and helpers for processing payments through multiple providers with a consistent PHP API.
Reality check first: xola/omnipay-bundle is a Symfony bundle, not a Laravel package. Laravel's package auto-discovery will not register it, and there is no facade or service provider inside. A Laravel developer will get the most value by treating it as a reference implementation of how to wire Omnipay into a framework container, then porting those patterns into a ServiceProvider + config/omnipay.php. Use omnipay/omnipay (or league/omnipay) directly as the runtime dependency.
If you are in a Symfony app alongside Laravel work:
composer require xola/omnipay-bundle omnipay/common
Then register the bundle in config/bundles.php and publish gateway config:
# config/packages/omnipay.yaml
omnipay:
gateways:
stripe:
type: Stripe
parameters:
apiKey: '%env(STRIPE_KEY)%'
paypal:
type: PayPal_Express
parameters:
username: '%env(PAYPAL_USER)%'
password: '%env(PAYPAL_PASS)%'
signature: '%env(PAYPAL_SIG)%'
Where to look first (small codebase, ~4 files worth reading):
DependencyInjection/Configuration.php — the config tree; this tells you every knob exposed.DependencyInjection/OmnipayExtension.php — how gateways are turned into container services.Resources/config/services.yml — service IDs you can inject (omnipay.gateway.<name>).OmnipayBundle.php — trivial; confirms there are no compiler passes to worry about.First use case to validate the integration — a Stripe test purchase:
$response = $gateway->purchase([
'amount' => '10.00',
'currency' => 'USD',
'card' => $card, // Omnipay\Common\CreditCard
])->send();
if ($response->isRedirect()) {
return redirect($response->getRedirectUrl());
}
if ($response->isSuccessful()) {
// persist transaction
}
Laravel-native equivalent of "first use case":
// config/omnipay.php
return ['gateways' => [
'stripe' => ['type' => 'Stripe', 'parameters' => ['apiKey' => env('STRIPE_KEY')]],
]];
// AppServiceProvider::register()
$this->app->singleton('omnipay', fn ($app) => new GatewayFactory);
$this->app->bind('omnipay.gateway.stripe', fn ($app) =>
$app['omnipay']->create(
config('omnipay.gateways.stripe.type'),
config('omnipay.gateways.stripe.parameters')
)
);
1. Config-driven gateway selection. The bundle's real value is the idea of named gateways defined in config, injected by name. Recreate this in Laravel with a small GatewayManager rather than hardcoding new StripeGateway() in controllers:
class GatewayManager
{
public function gateway(?string $name = null): AbstractGateway
{
$name ??= config('omnipay.default');
$cfg = config("omnipay.gateways.{$name}");
return (new GatewayFactory)->create($cfg['type'], $cfg['parameters']);
}
}
2. Controller → Action → Job split. Payment flows are long-running and side-effect-heavy. Pattern:
PaymentController@store — creates a local payments row in pending, calls purchase(), stores the transactionReference, redirects.PaymentController@callback — calls completePurchase(), updates the row.ProcessPaymentWebhook job — reconciles asynchronous events (Stripe webhooks, PayPal IPN).Keeping pending rows before the gateway call is essential — the bundle does nothing to help you here, and Omnipay itself is stateless.
3. Multi-gateway / per-tenant routing. Because the Symfony bundle registers gateways as separate services, the natural Laravel analogue is a manager keyed by name, with the active gateway resolved from the tenant/currency/region on the model:
$gateway = $manager->gateway($order->tenant->gateway); // 'stripe', 'paypal', ...
4. Callback/return URL wiring. Omnipay gateways expect returnUrl and cancelUrl (or notifyUrl for webhook-style). Centralise them via a helper to avoid drift:
$gateway->purchase([
'amount' => $order->total,
'currency' => $order->currency,
'card' => $card,
'returnUrl' => route('payments.callback', $order),
'cancelUrl' => route('payments.cancel', $order),
])->send();
5. Testing with the Mock gateway. Omnipay ships Omnipay\Common\Gateway\MockGateway and FakeGateway, both of which satisfy the bundle's factory. In Laravel, bind a fake in tests:
$this->app->bind('omnipay.gateway.stripe', fn () =>
(new GatewayFactory)->create('Mock', ['returnUrl' => '/ok'])
);
Use $response->getMockResponse() style assertions — much faster than hitting Stripe sandbox in CI.
6. Logging / HTTP client swap. Omnipay\Common\Http\ClientInterface (PSR-18 in Omnipay 3) is the extension seam. Wrap GuzzleHttp\Client to log requests/responses into your laravel.log — invaluable for debugging gateway rejections, which otherwise surface as opaque InvalidResponseExceptions.
7. Symfony→Laravel bridging. If you're porting an existing Symfony app, the bundle's omnipay.gateway.* service IDs map cleanly to Laravel container bindings; keep the same IDs (omnipay.gateway.stripe) to minimise churn in ported code.
Compatibility is the biggest footgun. The last release is 2020-01-22, which targets Symfony 4/5 and Omnipay 3 on PHP 7.2/7.4. On PHP 8.2+ or Symfony 6/7 you will very likely hit deprecations or hard failures in the DI extension. Laravel 10/11 (PHP 8.1+) — do not try to use the bundle itself; use omnipay/omnipay direct and copy the patterns.
Omnipay 2 ↔ 3 API breakage. Lots of blog posts and the old barryvdh/laravel-omnipay package target Omnipay 2. Differences in Omnipay 3:
CreditCard still exists but gateways use CardInterface.completePurchase() parameter shape changed (notably for PayPal and SecurePay).client is now PSR-18-ish (ClientInterface), not Guzzle 5/6-specific.ResponseInterface methods renamed in places (getRedirectResponse(), etc.).Pinning omnipay/common:^3 and matching each gateway package to Omnipay 3 is mandatory; mixing a gateway that needs Omnipay 2 will silently fail at runtime.
No Laravel facade, no auto-discovery, no config publishing. If you tried to php artisan vendor:publish, there's nothing to publish. The Symfony bundle relies on %env(VAR)% parameter resolution; Laravel uses env(), which behaves differently under config:cache — always resolve env() in config files, never in services.
Secrets in YAML. The Symfony bundle's example config puts keys in config/packages/omnipay.yaml. In Laravel, keep keys in config/omnipay.php reading env(), and never commit .env. Rotate keys if they were committed during the Omnipay 2→3 upgrade.
Exception handling is your job. Omnipay throws InvalidCreditCardException, InvalidResponseException, GatewayException — the bundle does nothing to translate them. Wrap ->send() in a try/catch that logs the request/response and returns a user-safe error; never let raw gateway messages hit the UI.
Idempotency is not built in. Omnipay has no idempotency-key concept. For purchase() retries (network timeouts, double-clicks), you must record intent in a payments table with a unique idempotency_key and check it before calling the gateway — otherwise you'll double-charge during a transient 500 from the gateway.
Webhooks vs redirects. Omnipay is fundamentally redirect/completePurchase-oriented. Modern gateways (Stripe especially) expect webhook-driven confirmation. Treat completePurchase as a hint, and always reconcile against the webhook payload in a queued job before marking an order paid. A redirect return alone is spoofable.
3-D Secure / SCA flows. Omnipay supports Secure3D via gateway-specific methods; the bundle gives you the configured gateway but nothing else. For EU/UK Stripe you need purchase() → confirm()/completePurchase() handling and to persist the 3DS ref between requests.
Debugging checklist when a payment silently fails:
$response->getData() before casting.returnUrl is an absolute https:// URL — many gateways reject relative or http:// URLs with a confusing InvalidResponseException.omnipay/common version matches the gateway package's composer.json.Extension points that survive a Symfony→Laravel port:
GatewayFactory to pre-register your gateways and defaults (return URLs, currency).ClientInterface for logging/retry/metrics.AbstractGateway decorator is rarely worth it — prefer a thin application-level PaymentService that owns the domain logic.Don't fork the bundle unless you're staying on Symfony. Nine stars, zero dependents, and a 2020 release means it's effectively unmaintained; the cost of forking for PHP 8 / Symfony 6 fixes is higher than the ~40 lines of Laravel glue it saves.
How can I help you explore Laravel packages today?