sylius/payments-bundle
The Sylius Payments Bundle provides the payment foundation for Sylius e-commerce stores, including payment method management, gateway integrations, and transaction handling for checkout and order workflows. It offers extensible services and interfaces for building custom payment providers.
First, confirm you are the intended reader. Despite being catalogued alongside Laravel packages, this is a Symfony bundle for Sylius, not a Laravel package. There is no service provider, no config/payments.php, no Eloquent models. It depends on the Symfony DI container, Doctrine ORM, the Sylius Resource/Registry system, and Payum for gateway abstraction. If your app is Laravel, stop here — the correct comparators are laravel/cashier, stripe/stripe-php, or omnipay/*.
Second, confirm the package is actually in play. The repo is marked [READ-ONLY] For BC purposes — payment functionality was folded into Sylius core (Sylius\Bundle\PaymentBundle, Sylius\Component\Payment) from Sylius 1.0 onward. So there are exactly two audiences:
Payment, PaymentMethod, gateway config, and the payment state machine, then implement against Sylius core.Verify which case you are in:
composer show sylius/payments-bundle # present? note the version + constraint set
grep -rn "Sylius\\\\Bundle\\\\PaymentsBundle" app/ src/ config/ 2>/dev/null
php -v && composer show sylius/sylius | head -3
If PHP is 5.6–7.2 with Symfony 2.8–3.4, the bundle is load-bearing and you are on the legacy path. If PHP is 8.x, it will not resolve at all.
Where to look first, in this order:
Payment entity and its state field — this is the shared vocabulary between checkout, support, and finance. Enumerate every state and transition before touching anything.PaymentMethod + gateway configuration — per-channel availability and the gateway string that selects a Payum factory. This is where each live PSP is wired.winzou_state_machine graph sylius_payment in the bundle's Resources/config/) — transitions, guards, and after callbacks that trigger capture/status actions.gateways config in app/config/config.yml — factory, payum.action lists, storage config, and after_url/cancel_url for offsite redirects.app/Resources/SyliusPaymentBundle/views/ and any form theme overrides of the payment step.First use case to walk end to end: place an order with a single payment method, complete checkout, and trace one payment record through new → pending → completed. Then do the same with a deliberately failing gateway response to see failed. If both paths work on a production-shaped DB copy, you have the regression harness you need before any migration work.
The daily workflow is "operate the legacy layer, don't extend it." New payment feature requests should be redirected to a migration ticket. If work is genuinely unavoidable this quarter, these are the patterns.
Gateway registration (the extension point). Gateways are Payum factories, registered as a service and referenced by name from PaymentMethod.gateway. A custom gateway looks like:
// src/AppBundle/Payment/Gateway/Factory/MyPspGatewayFactory.php
class MyPspGatewayFactory extends AbstractGatewayFactory
{
public function getName() { return 'my_psp'; }
protected function getConfig() {
return ['payum.default_options' => [
'api_key' => '', 'sandbox' => true,
]];
}
}
// Resources/config/services.yml
services:
app.payment.gateway.factory.my_psp:
class: AppBundle\Payment\Gateway\Factory\MyPspGatewayFactory
parent: sylius.payum.gateway_factory.abstract
tags: [{ name: payum.gateway_factory, factory_name: my_psp }]
Then attach Payum actions (CaptureAction, StatusAction, or a custom one implementing ActionInterface + ApiAwareInterface) to that factory. The StatusAction is the part that actually reads the PSP response and drives the state machine — if payments get stuck in pending, look there first.
State-machine-driven side effects. The canonical pattern is to hook business logic into transitions rather than into controllers:
winzou_state_machine:
sylius_payment:
callbacks:
after:
notify_customer_on_failure:
on: ["failed"]
do: ["@app.payment.notifier", "notifyFailure"]
reconcile_on_complete:
on: ["completed"]
do: ["@app.payment.reconciler", "markSettled"]
This keeps the payment path identical across webhook, CLI reconciliation, and admin manual action. It also means your finance integration is coupled to state names — treat new / pending / completed / failed / refunded as a public API contract.
Multiple payments per order. Orders hold a collection of payments; refunds and partial captures are modelled as additional payments or partial transitions. In practice most teams implement partial refunds as a new payment record pointing at the same order, with the state machine tracking the delta. Decide this convention explicitly and write it down — mixing "mutate the original payment" and "append a refund payment" across gateways is the single most common source of reconciliation pain.
Integration via decoration, not subclassing. When you need to alter behaviour, decorate the bundle service rather than extending its classes:
services:
app.payment.state_resolver:
class: AppBundle\Payment\StateResolver
decorates: sylius.payment.state_resolver
arguments: ['@app.payment.state_resolver.inner']
Subclassed entities and forms are far harder to migrate; decorators map one-to-one onto core services.
Migration pattern — the actual "day-to-day" leverage. Use the bundle as the specification for the core port. For each live gateway, build a parity harness first, then port:
after_url, its webhook handling, its state transitions.The read-only banner is the dominant constraint. No upstream fixes, no CVE channel, no composer update that lands a patch. Pin hard:
composer.lock, vendor the tree into a locked container image, and treat any dependency bump as a change requiring payment-path regression tests.State names are an API, not an implementation detail. Finance reporting, dunning, refunds, and customer support macros all key off the state string. Before migrating, snapshot the distinct values present in production and backfill/map them explicitly. A migration that renames completed to captured breaks revenue reporting on day one.
Order of after callbacks is undefined enough to hurt. If you have both a notification callback and a reconciliation callback on the same transition, do not assume ordering. Make each callback idempotent and independent; chain them explicitly if sequence matters. Debug with winzou graph dump rather than reading YAML:
app/console debug:state-machine sylius_payment
Destructive schema diffs are the biggest scheduling risk. Run the Doctrine schema diff against a production-shaped copy before estimating the migration. Table renames and column type changes on the payments table need a rehearsed rollback, not a --force.
Offsite redirects and after_url are the classic lost-payment source. Users who redirect to the PSP and never return leave payments in pending forever. You need a sweeper — a cron or console command that polls PSP status for stale pending payments — and it must exist before migration, because it is also what makes the core port safe.
Templates and forms break silently on upgrade. The payment step form is theme-overridden more often than any other part of the bundle. Grep for SyliusPaymentBundle in app/Resources/views and check each file against core's new markup as part of the port.
PHP 5.6–7.2 means no opcache JIT, no typed properties, and no modern runtime tuning. Do not spend effort optimizing the payment path itself — p95 is PSP-network-bound. Scale horizontally at the app tier and leave the gateway latency as the known floor.
Bus factor is the real operational risk. Sylius/Payum payment internals — action pipeline, gateway factory registration, state machine guards — take roughly 2–4 weeks for a competent Symfony engineer to become productive on, and the skill set is thinning. Name a primary owner and a documented secondary; write the state machine and gateway flows into a runbook as part of the migration, not after it.
The one clean leverage point: as a reading reference, this bundle is genuinely useful. Its Payment/PaymentMethod model, refund handling, and state transitions are the spec you would otherwise write from scratch — and that work now ships in Sylius core. Read the source, cite it in your migration design doc, and do not add it to composer.json.
How can I help you explore Laravel packages today?