softspring/payment-bundle
Softspring Payment Bundle adds configurable payment workflows to Symfony applications, with gateway abstractions, payment method management, and Doctrine-backed transaction handling for one-time and recurring payments.
First, a critical framing fact: softspring/payment-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no php artisan vendor:publish, no config/payment.php, no facades, and no Eloquent integration. If you are in a Laravel codebase, "getting started" means treating the source as a reference implementation to lift the payment/payment-method model and gateway abstraction into your own domain layer — not installing it. If you are in a Symfony app, the normal bundle install path applies.
Minimal Symfony setup:
composer require softspring/payment-bundle
// config/bundles.php
Softspring\PaymentBundle\SoftspringPaymentBundle::class => ['all' => true],
# config/packages/softspring_payment.yaml
softspring_payment:
# inspect src/DependencyInjection/Configuration.php for the real keys
Where to look first, in this order — with a low-signal package, the source is the documentation:
src/DependencyInjection/Configuration.php — the authoritative config tree. Every key you can set lives here.src/Resources/config/*.yaml — service definitions. This is the real public surface: actual class names, constructor arguments, and wiring.src/Entity or src/Model) — Payment, PaymentMethod, and any repositories. These are Doctrine entities you will persist and query.tests/ if present — the most reliable usage examples; if absent, be suspicious of everything.First end-to-end use case to get working: create a PaymentMethod for a customer, create a Payment in a pending state against that method, redirect the user to the gateway, then handle the return/webhook and transition the payment to paid or failed. Do this against a single test gateway before wiring a real PSP.
Model it as an entity + gateway pair, not a checkout flow. The bundle gives you persistence and a provider abstraction; it does not give you a full checkout. You will own the controller that starts the payment and the controller that receives the callback. Let the bundle own state and gateway calls.
Drive the payment lifecycle from one state-machine service. Keep pending → authorized/paid → captured, and pending → failed/cancelled/refunded transitions inside a single service method. Do not sprinkle setStatus() calls across controllers. In Laravel terms, this is your Action class — make it the only writer of payment state.
Webhook-first, redirect-second. Treat the browser redirect back from the PSP as a UX nicety; the server-to-server webhook is the source of truth. Pattern:
// callback controller — verify, then hand off
$payment = $paymentRepository->findByGatewayReference($request->request->get('reference'));
if (!$provider->verifySignature($request)) {
throw new BadRequestHttpException();
}
// idempotency: only transition if the payment is still pending
$paymentStateMachine->transitionTo($payment, PaymentStatus::PAID);
Wrap it behind your own interface, even in Symfony. Define an internal PaymentGateway contract and adapt the bundle's providers to it. Because this package has zero dependents and no community signal, you want the option to swap it out — or drop it entirely — without rewriting controllers. This also lets you run the bundle in parallel with a known-good SDK during evaluation.
Idempotency at the boundary. Every webhook and every gateway-return handler must be idempotent, keyed on the PSP reference or a client-supplied idempotency key. Payment webhooks will be replayed. Write a test that fires the same webhook twice and asserts a single state transition.
Persist gateway references, not raw payloads. Store the PSP's transaction ID and a redacted snapshot of the response, not card data or full request bodies. This keeps you out of PCI scope and gives you an audit trail.
Route by currency/country when you go multi-gateway. The abstraction exists to support this: a PaymentGatewayResolver that selects a provider from (country, currency, amount) is the payoff for adopting a gateway-agnostic layer instead of a single PSP SDK.
Laravel integration, if you must. Run it as a separate Symfony service behind a queue or HTTP boundary (a small "payments" microservice) and call it from Laravel. Do not attempt to port the Doctrine entities into Eloquent and wire the bundle's container config — that is a one-to-three-week port with no guarantee the semantics survive.
src/DependencyInjection/Configuration.php and src/Resources/config/*.yaml are the only reliable spec for keys and services. READMEs on low-signal packages drift.UPDATE ... WHERE status = 'pending') rather than read-modify-write in PHP.composer.json and every adapter, check the license (MIT vs. proprietary), and confirm the PHP/Symfony/Doctrine version matrix against your stack. Never let it become the only path to money movement during evaluation.How can I help you explore Laravel packages today?