payum/payum-laravel-package
Payum Laravel Package integrates the Payum payment library with Laravel, making it easier to configure payment gateways, handle captures and notifications, and manage payment workflows through Laravel-friendly services and configuration.
Install and publish.
composer require payum/payum-laravel-package
php artisan vendor:publish --provider="Payum\LaravelPackage\PayumServiceProvider"
php artisan migrate
This gives you three things to look at, in this order:
config/payum.php — the whole package is essentially this file plus two bindings. It declares your gateways (one array entry per gateway, keyed by the name you'll use in getGateway()) and your storages.PayumServiceProvider — read register()/boot() once. It binds payum (the Payum\Core\Payum instance), a token storage, a gateway-config storage, and an HTTP request verifier. Everything the facade gives you comes from here.payum_token (persistent security tokens for notify/capture URLs) and payum_gateway_config.Reality check before you commit: last release was 2020-10-31 and there are zero dependents. On Laravel 9+/PHP 8 you will likely hit constraint conflicts. The pragmatic path for a new project is
composer require payum/payum payum/*-gatewayand copy the ~80-line service provider out of this repo. Use this package as-is only on older Laravel.
First use case: a sandbox capture.
Configure a gateway:
// config/payum.php
'gateways' => [
'paypal' => [
'factory' => 'paypal_express_checkout',
'username' => env('PAYPAL_USERNAME'),
'password' => env('PAYPAL_PASSWORD'),
'signature' => env('PAYPAL_SIGNATURE'),
'sandbox' => true,
],
],
Then drive it from a controller. Payum's control flow is exception-based: execute() either returns normally or throws a Reply object that represents an outbound HTTP action. The very first thing to internalise is that you must translate replies into Laravel responses yourself.
use Payum\Core\Payum;
use Payum\Core\Request\Capture;
use Payum\Core\Reply\HttpRedirect;
public function pay(Payum $payum)
{
$payment = new \Payum\LaravelPackage\Model\Payment();
$payment->setNumber(uniqid());
$payment->setTotalAmount(1234); // minor units: 12.34 EUR
$payment->setCurrencyCode('EUR');
$payment->setClientEmail(auth()->user()->email);
$payment->save();
try {
$payum->getGateway('paypal')->execute(new Capture($payment));
} catch (HttpRedirect $reply) {
return redirect()->away($reply->getUrl());
}
return redirect()->route('checkout.done', $payment);
}
If that renders a PayPal page in sandbox, you've validated config, gateway factory, storage, and reply handling in one shot. Everything else is variations on this loop.
1. Centralise reply translation. Don't repeat try/catch in every controller. One helper that maps Payum replies to Laravel responses pays for itself immediately:
class PayumResponse
{
public static function from(callable $fn)
{
try {
$fn();
return null;
} catch (HttpRedirect $r) {
return redirect()->away($r->getUrl());
} catch (HttpPost $r) {
return response()->view('payum::post', ['url' => $r->getUrl(), 'fields' => $r->getFields()]);
} catch (HttpResponse $r) {
return response($r->getContent(), $r->getStatusCode(), $r->getHeaders());
}
}
}
HttpPost is the one people forget — several gateways return an auto-submitting HTML form (3-D Secure, some redirects). You need a tiny Blade view that POSTs on load.
2. The return/notify pair. Payment flows are two-legged. The return action is user-facing (browser comes back); the notify action is server-to-server. Generate notify URLs from the token factory so they're stateless and tamper-proof:
$token = $payum->getTokenFactory()->createNotifyToken('paypal', $payment);
return $token->getTargetUrl(); // hand this to the gateway as notify_url
Then in the notify controller, verify the request before trusting anything:
Route::post('/payum/notify', function (Payum $payum) {
$request = \Payum\Core\Http\Request::createFromGlobals();
$token = app('payum.http_request_verifier')->verify($request);
$payment = $payum->getGateway($token->getGatewayName())->execute(new Sync($token));
// Sync pulls fresh status from the provider
});
Exclude this route from VerifyCsrfToken::$except — the gateway is the caller, not a browser session.
3. Model choices. For prototypes, Payum\LaravelPackage\Model\Payment works out of the box (it JSON-casts details). For real apps, extend it or implement PaymentInterface + DetailsAggregateInterface on your own Order model and point the Eloquent storage config at it. This is where the package earns its keep: you keep your domain model and Payum just persists details as an opaque bag.
4. Drive UI from GetHumanStatus. Don't invent your own status enum:
$status = $payum->getGateway($name)->execute(new GetHumanStatus($payment))->getValue();
// captured, authorized, pending, failed, ...
Persist it on a column so your admin screens don't re-poll the provider.
5. Gateway config in the database. The payum_gateway_config table + GatewayConfigStorage exist so you can add merchant accounts at runtime (multi-tenant SaaS, per-store credentials) without a deploy. For single-tenant apps, ignore this table entirely and keep config in config/payum.php — it's simpler and config-cacheable.
6. Custom actions and extensions for cross-cutting concerns. Logging, idempotency, metrics — attach them to the gateway rather than smearing them across controllers:
$gateway = $payum->getGateway('paypal');
$gateway->addExtension(new LoggingExtension($logger)); // implements ExtensionInterface
$gateway->addAction(new EnsureIdempotentAction()); // implements ActionInterface
Actions are the idiomatic Payum extension point; a "record every request/response" action saves hours when a provider starts behaving oddly.
7. Testing. Don't hit sandbox in CI. Build a Payum\Core\Gateway with a stub capture action, or bind a fake Payum implementation into the container and assert your controller produced the right redirect()->away(...). The exception-based API makes this pleasant — you assert on the thrown HttpRedirect, not on rendered HTML.
Amounts are minor units. setTotalAmount(1234) is €12.34. This is the single most common bug in Payum integrations — you'll charge 100× or 1/100× and only notice in production. Always derive from an integer column (price_cents), never from a float.
Replies are exceptions, and Laravel's handler doesn't know them. If a HttpRedirect escapes a controller uncaught, Laravel renders a 500. Either wrap every execute() or add a handler in app/Exceptions/Handler.php that maps ReplyInterface to a response centrally. The latter is less error-prone.
Token table growth. payum_token gets a row per notify URL, forever. Add a scheduled command that prunes tokens older than your longest-lived payment window. Nothing in the package does this for you.
Behind a load balancer, notify URLs are wrong. The token factory builds absolute URLs from the current request. With TrustProxies misconfigured or APP_URL unset, you'll hand providers http://internal-host/.... Set APP_URL and configure trusted proxies before going anywhere near staging.
config:cache and closures. If you put a closure in config/payum.php (easy to do for dynamic credentials), php artisan config:cache blows up. Keep config strictly scalar; use env vars or a DB lookup.
Gateway config keys are not uniform. sandbox for PayPal, testMode/sandbox depending on factory version, publishable_key vs api_key for Stripe. There is no unified schema — copy from the specific gateway's README, not from Payum's docs.
Storage + getFirstModel(). Nested requests wrap models. $capture->getModel() gives you the payment; getFirstModel() can give you the request. When in doubt, read the model off the Reply you caught.
Old dependency graph. The 2020 release pins Guzzle and symfony/http-foundation ranges that fight with modern Laravel. If composer update explodes, that's why. Forking the provider and dropping the constraint is usually faster than hunting for a compatible set.
No observability built in. There's no log driver, no debug toolbar integration. Add a Payum ExtensionInterface that logs every execute() call and the resulting status; you'll want it the first time a webhook silently no-ops.
Extension points worth knowing: ExtensionInterface (wrap every action — perfect for logging/idempotency), ActionInterface (add new capabilities to a gateway), custom TokenStorage (Redis instead of MySQL, if token lookups get hot), and custom storage implementations if Eloquent's overhead bothers you.
Verdict for day-to-day use. The core Payum library is genuinely good — the request/reply/action architecture scales from a single PayPal button to multi-gateway marketplaces with escrow. This Laravel wrapper is thin and dated but still the fastest way to get payum bound and configured in an older app. Budget a day for dependency wrangling on anything newer than Laravel 7, and expect to write your own reply-to-response layer and token pruning regardless.
How can I help you explore Laravel packages today?