flux-se/payum-stripe-bundle
Integrates the Stripe payment gateway into Symfony applications via Payum, providing gateway factory configuration and actions for handling Stripe payments, captures, and refunds through a unified API.
Install and register the bundle:
composer require flux-se/payum-stripe-bundle:^3.0.1
// config/bundles.php
return [
// ...
FluxSE\PayumStripeBundle\FluxSEPayumStripeBundle::class => ['all' => true],
];
Note: the upstream repository is
FLUX-SE/PayumStripeBundleand is still shipping releases (v3.0.1 added Symfony 8 support). A Sylius-maintained fork also exists atSylius/PayumStripeBundle. Whichever you pulled, the class is<Vendor>\PayumStripeBundle\<Vendor>PayumStripeBundle— checkvendor/.../src/*Bundle.phpto know which one you actually have before editingbundles.php.
Symfony 8 requires v3.0.1 or later; v3.0.0 and earlier will fail dependency resolution on Symfony 8. On Symfony 6.4/7.x you are free to pin v3.0.0. Confirm with
composer show flux-se/payum-stripe-bundleand check that the Payum core constraint resolves for your Symfony version.
Put credentials in env — never in YAML:
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
Then register the Payum gateway:
# config/packages/payum.yaml
payum:
gateways:
stripe_checkout_session:
factory: stripe_checkout_session
publishable_key: '%env(STRIPE_PUBLISHABLE_KEY)%'
secret_key: '%env(STRIPE_SECRET_KEY)%'
webhook_secret: '%env(STRIPE_WEBHOOK_SECRET)%'
Confirm the factory is registered before going further — a typo here fails later as an opaque RequestNotSupported:
bin/console debug:container --tag=payum.gateway_factory | grep -i stripe
bin/console debug:router | grep -i payum
Where to look first (in this order):
src/DependencyInjection/Configuration.php — the authoritative list of gateway options.src/Action/CaptureAction.php (builds the Checkout Session) and src/Action/NotifyAction.php (handles the webhook). These two files define 90% of the runtime behavior.bin/console debug:router | grep payum — the capture / notify / after routes you paste into Stripe.First use case — a one-off charge. Execute Capture with a Payum token; the action creates a Stripe Checkout Session, writes the session id into payment.details, and replies with an HTTP redirect to checkout.stripe.com. Card entry, 3-D Secure and SCA all happen on Stripe's hosted page.
use Payum\Core\Payum;
use Payum\Core\Reply\HttpRedirect;
use Payum\Core\Request\Capture;
public function pay(Payum $payum, string $token): Response
{
$gateway = $payum->getGateway('stripe_checkout_session');
$reply = $gateway->execute(new Capture($token));
if ($reply instanceof HttpRedirect) {
return new RedirectResponse($reply->getUrl());
}
// Payment was already captured — show the order instead of re-charging.
return $this->redirectToRoute('order_complete');
}
Capture vs Authorize. Capture maps to Stripe's capture_method: automatic — money moves immediately. Use Authorize when you only want to charge on shipment: it creates a manual-capture PaymentIntent, and you later execute Capture on the authorized payment to settle it. Pick one per payment method; mixing them in the same gateway config confuses refund/void flows.
In Sylius you rarely touch Payum directly. Enable the Stripe Checkout Session factory on the payment method, paste the three keys into the gateway-config form, and let Sylius' checkout run capture → redirect → after-url. Fulfilment belongs in sylius.order.post_complete / sylius.payment.post_complete listeners, not in the gateway code. If you are on Symfony 8, verify the Sylius/Payum versions your Sylius release allows before bumping the bundle to v3.0.1.
The webhook is the source of truth, the redirect is UX. Users close tabs, lose connectivity, or hit the back button on Stripe's page. Grant access / ship goods on checkout.session.completed or payment_intent.succeeded (via NotifyAction), or on an explicit Status/Sync request that queries Stripe by the stored session id. Treat the success_url return as a nice-to-have.
Idempotency is your job. Stripe delivers at least once and retries on any non-2xx. Guard domain listeners by checking $payment->isCaptured() / details['status'] before fulfilling, and add a unique index on your order reference. The bundle's actions are usually idempotent; your listeners are usually not.
Metadata for reconciliation. Whatever CaptureAction puts into the Session (or PaymentIntent) metadata comes back in the webhook payload. Add order_id / customer_id there — via a small decorator around the Stripe client or the action — so the notify handler can resolve your domain object without guessing.
Queue the post-payment work. Don't do emails, ERP sync or PDF generation inside the notify controller; dispatch a Symfony Message from a listener. Keeps the webhook response under Stripe's timeout and gives you retries via Messenger.
Refunds/voids. These aren't the bundle's focus. Read the PaymentIntent id out of payment.details, then call \Stripe\Refund::create(...) in your own service (or a custom Payum Refund action). Wrap it so the Stripe call is retryable and logged.
Local testing.
stripe login
stripe listen --forward-to https://myapp.test/payment/notify
stripe trigger checkout.session.completed
Cards worth memorizing: 4242 4242 4242 4242 (success), 4000 0025 0000 3155 (3DS required), 4000 0000 0000 0002 (declined), 4000 0000 0000 9995 (insufficient funds). Always run one end-to-end with stripe listen running — that's the only way to see whether your notify route actually resolves the payment.
SignatureVerificationException almost always means webhook_secret is wrong — test vs live, or the endpoint-level secret vs the account default. It also happens when a proxy strips the Stripe-Signature header; check trusted_proxies and any nginx config rewriting headers.Pending forever. Add a scheduled command that voids/expires stale payments (Stripe's session-expire API) so your order pipeline doesn't fill with zombies.success_url / cancel_url are absolute. Without framework.trusted_proxies + trusted_headers, Symfony emits http://… and Stripe refuses it in live mode. Symptom: works locally, fails in staging.pk_test_ publishable key with an sk_live_ secret produces "No such Checkout Session" / "No such customer" rather than a clean auth error. Validate that all three env vars share the same prefix.TotalAmount must be the integer minor-unit value, and CurrencyCode must be present. Missing currency or an amount in the wrong exponent (JPY, KRW) fails at the Stripe API, not in your code.templates/bundles/... and keep the redirect instant — no JS round-trip needed.debug:router | grep payum for the endpoints, debug:container | grep -i stripe for services and the factory, \Stripe\Stripe::setLogger($logger) at debug level in dev, and the Stripe Dashboard's webhook page for delivery attempts and response bodies. Between those four you can diagnose almost anything.RequestNotSupported. Usually means the gateway factory isn't registered (wrong factory: name, bundle not in bundles.php, or config not loaded). Confirm against debug:container --tag=payum.gateway_factory before touching the actions.>=3.0.1, and a composer update that moves Symfony without moving the bundle will produce confusing container/Payum errors rather than a clean conflict. Pin the bundle and bump Symfony and the bundle in the same commit.FLUX-SE/PayumStripeBundle is still actively released (v3.0.1), so it is not abandoned — but a Sylius-maintained fork also exists, and the two can diverge. composer show to see which you have, pin a major, and read the changelog when you cross majors. Dependency count is tiny and the docs are thin — budget an hour reading src/Action/ before you extend anything non-standard.payment_method_types) by decorating CaptureAction or the Stripe client service, not by forking the bundle. A decorator that injects metadata['order_id'] into SessionCreateOptions is typically all you need — and that metadata is what makes the webhook handler trivial six months later.How can I help you explore Laravel packages today?