Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Payum Bundle Laravel Package

payum/payum-bundle

PayumBundle integrates Payum payment processing into Symfony, supporting 35+ gateways with YAML configuration, storage integration, and a secure capture controller. It powers payments for Sylius and works with any Symfony app.

View on GitHub
Deep Wiki
Context7

Getting Started

Do not install this in a Laravel app. payum/payum-bundle is Symfony-only glue: it registers DI services, compiler passes, routes via Symfony routing, a Twig form type (CreditCardType), and a profiler collector. Nothing in it is usable outside symfony/framework-bundle. What you actually want is the framework-agnostic engine underneath it, payum/core, plus one gateway package.

composer require payum/core payum/stripe   # or payum/paypal-express-checkout-nvp, payum/authorize-net, etc.
# NOT payum/payum-bundle

Read in this order:

  1. The bundle README's config block — it is the clearest one-page summary of the three moving parts: storages, security.token_storage, gateways. Mentally translate that YAML into a Laravel config array.
  2. payum.gitbook.io/payum — Get it started and Core concepts. The Symfony page is the closest thing to a config reference; everything below the config layer is portable.
  3. Payum\Core\Model\Payment, Payum\Core\Request\Capture, Payum\Core\Request\GetHumanStatus — read those four classes and you understand 80% of the API.
  4. makasim/PayumBundleSandbox — useful purely to see the request/response flow (capture → redirect → done → status), even though the code is Symfony.

First use case — prove the loop with offline. Before touching a real PSP, wire the offline gateway and a payments table, execute a Capture, then a GetHumanStatus. This forces you to solve the two things that will bite you later (redirect replies and persistence) with zero credentials and zero network.

$payment = Payment::create(['number' => uniqid(), 'total_amount' => 123, 'currency_code' => 'EUR']);
$gateway->execute(new Capture($payment));          // offline: no redirect, just flips details
$gateway->execute($status = new GetHumanStatus($payment));
$status->getValue();                               // 'captured'

Then swap the config for a real factory and the same two lines still work. That is the whole selling point of the package.

Implementation Patterns

Wire the builder once, in a service provider

PayumBuilder is the DI-free equivalent of the bundle's payum.yaml. Bind the result as a singleton so token storage and gateway config are shared.

// app/Providers/PayumServiceProvider.php
$this->app->singleton(Payum::class, function () {
    return (new PayumBuilder())
        ->addDefaultStorages()                       // filesystem storage for Payment/Token — dev only
        ->addGateway('offline', ['factory' => 'offline'])
        ->addGateway('stripe', [
            'factory'         => 'stripe_checkout',
            'publishable_key' => config('services.stripe.key'),
            'secret_key'      => config('services.stripe.secret'),
        ])
        ->setTokenStorage(new EloquentTokenStorage())
        ->getPayum();
});

Expose a thin PaymentGateway service (capture(), status(), refund()) wrapping $payum->getGateway($name) — never let GatewayInterface leak into controllers. It makes the later swap to a fake gateway trivial.

Model storage: Eloquent, not the filesystem driver

The bundle's filesystem storage is a demo affordance. In Laravel, back it with a payments table where Payum's mutable blob lives in a json column:

class Payment extends Model implements \Payum\Core\Model\PaymentInterface {
    use \Payum\Core\Model\PaymentTrait;              // provides getDetails/setDetails etc.
    protected $casts = ['details' => 'array'];
    protected $fillable = ['number','total_amount','currency_code','description','client_email','details'];
}

Implement Payum\Core\Storage\StorageInterface (support, find, updateModel, deleteModel) — extending AbstractStorage gives you the last two. Two methods of real work.

The capture controller is a redirect factory

Capture does not return anything. It either mutates details or throws a reply exception carrying the next step. Translate replies into HTTP:

try {
    $gateway->execute(new Capture($payment));
    return redirect()->route('checkout.done', $payment);      // offline / no-op
} catch (HttpRedirect $e) {
    return redirect()->away($e->getUrl());                    // off-site PSP
} catch (HttpResponse $e) {                                   // on-site form / 3DS
    return response($e->getContent(), $e->getStatusCode());
} finally {
    $payment->save();                                          // ALWAYS persist details
}

ReplyToReplyException is the escape hatch when an action needs to run after the browser comes back — new ForceReply(new Capture($payment)).

Done / notify endpoints

Both are the same three steps: verify the token, re-execute the request, recompute status.

  • Done (?token=…): use TokenFactory::createToken() to build return URLs, and an HttpRequestVerifier to validate them. Tokens must be single-use — delete after verify.
  • Notify (PSP webhook): verify the token; if the PSP cannot carry a token, wrap the execute in NotifyUnsafe/Notify and treat it as untrusted — the whole point of the token is preventing a stranger from forcing a capture.
  • Persist before responding, and respond 200 fast; do the bookkeeping in a queued job keyed on payment_id.

Status is derived, never stored

GetHumanStatus maps gateway details to new | pending | authorized | captured | refunded | canceled | failed | expired | unknown. Run it on read (a PaymentStatus accessor or a small status() method) rather than denormalising into a column — the mapping changes with gateway versions and a stale column will silently lie to you. Treat unknown as "look at the raw details", not as a failure.

Gateways are additive config

Authorize, Refund, Cancel, Payout are distinct request classes; not every factory ships actions for all of them. Check the gateway's docs package before you promise refunds in a UI. Compose a supports() helper on your service around the factory name.

Gotchas and Tips

  • Replies are exceptions. HttpRedirect, HttpResponse, HttpPostRedirect all extend Payum\Core\Reply\Reply, which extends \Exception. Any execute() call without a try/catch that returns a 500 in production is a missing catch, not a gateway bug. Wrap it once in a helper and it disappears forever.
  • Details must be persisted after every execute, including inside the catch. Eloquent's dirty tracking misses in-place array mutation — reassign ($payment->details = $payment->details) or save() unconditionally in finally.
  • Token and id_property. If you keep filesystem storage for tokens in dev, id_property must match the model field your code reads back. In production use a DB-backed token storage with a TTL; tokens are bearer credentials for a capture.
  • HttpRequestVerifier is not in core any more. It moved out of Payum\Core\Bridge\Symfony into the bundle in 2.6.0 — so in Laravel you implement HttpRequestVerifierInterface yourself (read the token query param, look it up, Token::getTargetUrl()), or feed it a PSR-7 request via nyholm/psr7.
  • Amounts are minor units and integers. 123 means €1.23. Never let a float near it. The bundle once shipped an iso4217 service for this; it was removed in 2.1.0 — use moneyphp/money or a plain integer cast.
  • Version drift is the main risk. 2.7.0 flipped service/routing config from XML to PHP/YAML and broke apps referencing the XML paths; 2.7.1 restored them as deprecated shims slated for removal in 3.0. This only matters if you inherit a Symfony app, but it also means blog posts and Stack Overflow answers from before 2.7 describe files that no longer exist. Pin exact versions and read UPGRADE-3.0.md.
  • Symfony/PHP support outruns the gateways. The bundle advertises Symfony 8 / PHP 8.5, but individual payum/*-gateway packages lag. Run composer why-not before upgrading PHP on a payments codebase — a broken gateway package is not a hotfix you want mid-sprint.
  • payum/payum is a meta-package. Require payum/core plus only the gateways you actually use; the all-in-one pulls a large tree of abandoned PSP SDKs that will fail security audits and PHP upgrades.
  • Debugging. The bundle's profiler collector (PayumCollector) is Symfony-only. Replace it with a decorator over GatewayInterface that logs each request class, the factory name, and elapsed time, and behind a config flag dumps details before/after. That is the single highest-value 30 lines you will write against this package.
  • Extension points that carry over cleanly: implement Payum\Core\Action\ActionInterface (with supports() / execute()) and register via PayumBuilder::addAction(), plus addGatewayFactory() for a custom factory. This is how you inject idempotency keys, audit rows, or your own notification dispatch without forking a gateway.
  • Credit card collection is on you. The bundle's CreditCardType / CreditCardExpirationDateType forms and ObtainCreditCardAction assume Symfony forms and a session-stashed ObtainCreditCard reply. In Laravel you rebuild this in Blade/Livewire and implement the reply handling — budget for it; it is the largest chunk of the bundle you lose.
  • offline and the sandbox are for CI, not staging demos. Wire an offline gateway in config/payum.php under app()->environment('testing') so feature tests can assert status transitions without mocks.
  • Sylius runs its payments on this stack. When you hit an edge case (partial refunds, multi-gateway routing), Sylius's PaymentBundle integration is a far better reference than the bundle's own docs.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator