barryvdh/laravel-omnipay
Laravel wrapper for the Omnipay payment processing library, providing a service provider and facade to integrate multiple payment gateways. Configure gateways through config files and handle payments, refunds, and callbacks with a consistent, unified API.
This package is a thin Laravel service-provider wrapper around Omnipay. The v0.2.5 release is a compatibility bump (Laravel 10/11/12) with no API changes, so the day-to-day workflow is identical to earlier versions. Three things get you productive:
1. Install the wrapper and a driver. The wrapper ships no gateways — this is the most common first stumble:
composer require barryvdh/laravel-omnipay
composer require omnipay/paypal # or omnipay/stripe, omnipay/authorizenet, ...
composer require omnipay/dummy --dev # fake gateway, accepts everything
omnipay/dummy is the single highest-value thing to install early: it lets you build and test the whole order → purchase → persist-transaction flow before you have merchant credentials or a sandbox account.
2. Publish the config and wire credentials:
php artisan vendor:publish --provider="Barryvdh\Omnipay\ServiceProvider"
This produces config/omnipay.php with a default gateway key and a gateways array of named, independently-credentialed configurations. Each entry carries a driver (the Omnipay gateway class name) plus its own parameters and testMode flag. The config shape is the whole contract of this package — read it first, before the README, before the source.
'default' => 'paypal',
'gateways' => [
'paypal' => [
'driver' => 'PayPal_Express',
'username' => env('PAYPAL_USERNAME'),
'password' => env('PAYPAL_PASSWORD'),
'signature'=> env('PAYPAL_SIGNATURE'),
'testMode' => env('PAYPAL_TEST_MODE', true),
],
],
3. First call — a purchase through the default gateway:
use Omnipay\Omnipay; // facade alias registered by the package
$response = Omnipay::purchase([
'amount' => '10.00',
'currency' => 'USD',
'card' => $request->only(['number', 'expiryMonth', 'expiryYear', 'cvv']),
'description' => 'Order #1234',
])->send();
if ($response->isSuccessful()) {
$reference = $response->getTransactionReference();
} elseif ($response->isRedirect()) {
return redirect($response->getRedirectUrl()); // off-site flow
} else {
report(new PaymentFailed($response->getMessage()));
}
That is the entire mental model: config → gateway → a request object → send() → a response you branch on. Everything else in a real integration is your code, not this package's.
Never call the facade directly from a controller. Wrap each gateway (or each business operation) in a dedicated service class, bound in the container, with the gateway injected. This is what makes the abstraction actually pay off — swapping providers becomes a binding change:
final class PayPalGateway implements PaymentGateway
{
public function charge(Money $amount, Card $card): ChargeResult
{
$response = $this->gateway->purchase([...])->send();
return ChargeResult::fromOmnipay($response);
}
}
Bind the interface to the implementation in a service provider, choose the implementation from config, and let the rest of the app speak in your domain types (ChargeResult, Money) rather than Omnipay's response objects. If you skip this step, Omnipay's API leaks into every controller and the "swap providers without rewriting checkout" promise evaporates.
Select gateways by name, not by default, whenever more than one exists. Omnipay::gateway('stripe') returns the configured instance for that named entry. Use the default only for genuinely single-gateway apps; for multi-provider apps resolve the name from a domain rule (currency → gateway, market → gateway) and keep that rule in one place.
Persist the transaction before you branch on the response. A payments table with order_id, gateway, transaction_reference, status, amount_minor, currency, and the raw response payload (redacted) is not optional. Without getTransactionReference() stored durably, refunds, voids, and reconciliation become archaeology. Write the row in the same transaction that flips the order state.
Handle the three response shapes explicitly, every time. Omnipay responses are always one of: successful, redirect (off-site: PayPal Express, 3-D Secure), or failed. isSuccessful(), isRedirect(), getRedirectUrl(), getMessage(), getCode(). Code that checks if ($response->isSuccessful()) { ... } and falls through to an error path silently swallows the redirect case — that is a live bug in a lot of Omnipay integrations.
Complete off-site flows with completePurchase. For redirect gateways, the return URL handler calls completePurchase() with the parameters the gateway handed back, and that response is the authoritative one. Never mark an order paid based on the redirect alone — treat the browser round-trip as untrusted user input.
Test against omnipay/dummy in the suite. It supports purchase, authorize, capture, refund, and void with configurable outcomes, so you can drive every branch of your PaymentGateway implementation in feature tests without network calls. Reserve real sandbox credentials for a small, separate integration test set — and never in CI on every push.
Keep money as integers in your domain. Omnipay's amount parameter is a string, and its interpretation (decimal string vs. minor units) is driver-dependent. Convert once at the boundary (Money::toOmnipayAmount()), assert on the boundary, and never format currency inside the gateway service.
The package ships zero gateways. composer require barryvdh/laravel-omnipay alone gives you a config file and a facade that resolves nothing. The error surfaces late — as a "gateway not found" at runtime, not at install. Install drivers alongside the wrapper, always.
Pin the version and treat upgrades as mini-projects. This is 0.x, effectively single-maintainer, with no SLA or LTS policy. ^0.2.5 is fine; * is not. Budget a quarterly review of the wrapper and each Omnipay driver and Omnipay core — driver health is entirely independent of wrapper health, and the wrapper's Laravel 10–12 support tells you nothing about whether omnipay/authorizenet supports your PHP version.
env() only inside config files. The config is cached in production (php artisan config:cache), so env() calls elsewhere return null after caching. Put every credential in config/omnipay.php behind env(), and reference config('omnipay.gateways.paypal.username') from code. This is a Laravel-wide rule, but payment credentials are where it bites hardest.
testMode is per-gateway and easy to ship wrong. It lives inside each gateway's config entry, not at the top level. A production deploy that inherits a staging .env will happily send live charges to a sandbox or sandbox charges to a live merchant account — neither fails loudly. Assert on app()->environment() → testMode mapping in a boot check or a health endpoint.
Catch Omnipay's exceptions; don't let them become 500s. InvalidRequestException, InvalidResponseException, and driver-specific exceptions represent expected business outcomes — declined cards, malformed input — as much as infrastructure failure. Catch around send(), log with context (gateway, order id, response code — never the card number or full request body), and translate to a user-facing result.
Webhooks and callbacks are your problem entirely. This package does not verify signatures, deduplicate events, or retry. If you accept gateway webhooks: exclude the route from VerifyCsrfToken, verify the signature with the driver's own handler or a hand-rolled HMAC check, dedupe on the gateway's event id in a unique-indexed table, and process idempotently. A duplicate webhook replaying a "payment succeeded" is the classic way this goes wrong.
Idempotency keys are not provided. For any retry — network timeout, queue worker restart, user double-click — you need your own idempotency key tying an order to at most one charge. Omnipay passes driver-specific parameters through, so you can often supply the gateway's native idempotency key, but the wrapper won't do it for you and the parameter name differs per driver.
The facade is a manager, not a gateway. Methods like purchase() work because the manager forwards to the default gateway. Once you have more than one gateway configured, prefer Omnipay::gateway('name')->purchase(...) explicitly — relying on the forwarded default makes gateway selection invisible at the call site and painful to change later.
Config credentials are plain arrays. There's no validation, no required-key enforcement, no helpful error when username is missing — you get the driver's own exception, often cryptic. If you're onboarding multiple environments or gateways, add a small startup assertion or an artisan payments:check command that instantiates each configured gateway and confirms required parameters are present. Ten minutes of work that saves an hour of debugging on deploy day.
Extension point: bypass the config when you need to. The wrapper is a convenience layer, not a lock-in. Omnipay::create('Stripe') (or the driver's own Gateway::class) lets you build a gateway with runtime-chosen parameters — useful for per-tenant credentials or a gateway instance that doesn't belong in the static config. Keep using the config path as the default so the package stays the single source of truth, and reach for create() only where the config model genuinely doesn't fit.
Sandbox behavior is not production behavior. Different gateways differ wildly in how they report declines, what getMessage() returns, and whether isRedirect() fires. Validate every flow — authorize, capture, refund, void, redirect-complete, decline, timeout — against each gateway's sandbox before go-live, and again whenever you bump a driver.
How can I help you explore Laravel packages today?