sylius/payment-bundle
SyliusPaymentBundle integrates the Payments component into Symfony eCommerce applications, providing the payment foundation used across the Sylius ecosystem. MIT licensed, documented at docs.sylius.com, and maintained by the Sylius community.
Know what you are installing. sylius/payment-bundle is glue, not an engine. The actual payment domain model (Payment, PaymentMethod, PaymentInterface, GatewayInterface, state machine callbacks) lives in sylius/payment. This bundle only registers the Symfony DI services, config tree, forms, and gateway registry that wire that component into a Symfony 2.x kernel. If you are on Laravel, you cannot composer require this and expect it to boot — the bundle depends on Symfony\HttpKernel, DependencyInjection, and FrameworkBundle. Treat it as a reference architecture or a dependency of a legacy Sylius app you are maintaining.
Minimal install (legacy Symfony only).
composer require sylius/payment-bundle:^0.x # pin an exact tag, it is READ-ONLY
// app/AppKernel.php
$bundles = [
new Sylius\Bundle\ResourceBundle\SyliusResourceBundle(),
new Sylius\Bundle\PaymentBundle\SyliusPaymentBundle(),
];
Look here first, in this order:
DependencyInjection/Configuration.php — the real contract. It shows what keys exist under sylius_payment: (gateways, resources, driver).DependencyInjection/SyliusPaymentExtension.php + Resources/config/services.xml — the service IDs you will actually reference.DependencyInjection/CompilerPass/* — gateway services are collected by tag here; this is where "gateway not found" errors originate.vendor/sylius/payment/Model/PaymentInterface.php and the GatewayInterface in the component — the boundary you code against.First useful use case: wire a single offline/manual gateway into a checkout and drive a payment through new → pending → completed using the component's state machine. Create a PaymentMethod bound to your gateway name, create a Payment with an integer amount in minor units, call the gateway, then apply the transition. Verify the state machine config file exists in the bundle (Resources/config/state_machine.yml or similar) before assuming transition names — they differ between tags.
One service per gateway, selected by name. Do not switch on a string inside a controller. The intended pattern is:
// services.xml
<service id="app.payment.gateway.stripe" class="App\Payment\StripeGateway">
<tag name="sylius.payment.gateway" alias="stripe" />
</service>
Then resolve from the registry/GatewayFactory by the alias stored on the PaymentMethod. This is the compiler-pass-driven lookup the bundle exists to provide, and it is the single most transferable idea to a Laravel app (bind named gateways in a PaymentGatewayManager keyed by alias).
Wrap the state machine, don't call it from controllers. Create an application service (PaymentProcessor) that owns: load payment → check can($transition) → call gateway → apply($transition) → flush. Every controller, webhook, and console command goes through it. In Laravel this maps to a PaymentService + events; in Symfony, keep it a plain service so it is unit-testable without the kernel.
Webhook/callback controller pattern. The provider returns to a route; look the payment up by an unguessable token or hash stored in details, not by sequential ID. Then re-verify status with the provider's API rather than trusting the request body, and apply complete/fail idempotently (see gotchas).
$payment = $repo->findOneByToken($request->query->get('token'));
if (!$processor->isFinal($payment)) {
$processor->complete($payment, $request->query->all());
}
return new Response('OK');
Persist raw gateway responses in details. The component models details as a free-form array. Always store the provider's transaction reference, the raw response (redacted), and the request ID. When a reconciliation issue appears six months later, this is the only thing that saves you.
Integration tips.
PaymentMethod model with a gateway column, a PaymentGatewayManager resolving by alias, a Payment model with integer amount and a state column, and a transition guard service. sylius/payment itself is reasonably decoupled and can be evaluated standalone, but weigh that against omnipay/payum/laravel/cashier which have live ecosystems.Testing pattern. Register a DummyGateway/OfflineGateway under a test-only alias, and assert transitions and details contents rather than HTTP calls. The bundle's config tree makes swapping the alias per environment a one-line change.
It is READ-ONLY and effectively abandoned. Stars are low and dependents are zero — you are the only person hitting the bugs. Expect no fixes, no Symfony 4/5/6 compatibility in the older tags, and PHP version constraints that may conflict with modern tooling. Pin the exact tag, commit composer.lock, and plan an exit. Modern Sylius replaced this with Payum-based bundles; for anything greenfield, use Payum, Omnipay, or a maintained Laravel equivalent.
The README tells you nothing. There is no bundle-specific documentation — the link goes to generic Sylius docs. Your documentation is the source: Configuration.php for config keys, services.xml for IDs, compiler passes for tags. Budget time for reading, not for Googling.
Service IDs are not a stable API. They change between tags and get renamed during refactors. Never hardcode sylius.payment.gateway.registry in application code if you can inject by type/interface; if you must, isolate it behind one factory method so a version bump is a single-file change.
Config merges hide gateway typos. A misspelled key under sylius_payment.gateways frequently does not fail at container build — it fails at runtime as "gateway not found" or a null service. Add a container test or a compiler-pass assertion that every configured gateway alias resolves to a tagged service. Same for the driver key and resource mapping; those silently fall back and produce confusing Doctrine errors.
Amounts are integers in minor units. The component models money as integer amounts (cents). Mixing floats, or converting at the wrong layer, produces off-by-one-cent bugs that only show up in reconciliation. Format for display only at the view boundary, and store the currency code alongside the amount on every payment — do not infer it from the order at read time.
State machine transitions throw. apply() on an illegal transition raises an exception, and webhooks will be replayed by providers. Guard with can() before apply(), make the whole handler idempotent (return 200 for already-final states), and do not let a duplicate callback move a refunded payment back to completed. Treat the payment state as the source of truth, not the provider's message.
Never touch card data. The bundle gives you zero PCI tooling. The only sane pattern is a hosted payment page or tokenization at the provider, with your server handling redirects and webhooks. If you find yourself asking where to store a PAN, you have chosen the wrong integration.
Extension points, in order of safety: (1) your own GatewayInterface implementation tagged into the registry — fully supported; (2) event listeners on payment transitions — safe; (3) overriding bundle templates/routing via app config — version-fragile; (4) subclassing bundle services — avoid, they are not designed for it and the internals change.
Upgrade/exit path. Before you invest, diff the bundle's composer.json against your Symfony version and confirm the sylius/payment major is compatible. If either fails, do not patch the bundle — port your gateway implementations to Payum/Omnipay and keep your PaymentProcessor service, which is the part with actual business value.
How can I help you explore Laravel packages today?