idci/payment-bundle
IDCI PaymentBundle is a Symfony bundle for adding flexible payment handling to PHP applications, with abstractions for payment methods, gateway integrations, and configurable workflows to process and track transactions.
Install and wire it up
composer require idci/payment-bundle
Then in config/bundles.php:
IDCI\Bundle\PaymentBundle\IDCIPaymentBundle::class => ['all' => true],
Import the bundle routing (callback/notify endpoints are shipped by the bundle, so they must be reachable):
# config/routes/idci_payment.yaml
idci_payment:
resource: "@IDCIPaymentBundle/Resources/config/routing.xml"
prefix: /payment
The bundle persists payments and gateway configurations via Doctrine, so run migrations or doctrine:schema:update and confirm the new tables exist before writing any code.
Where to look first, in this order
PaymentManager (and its interface) — this is the front door. Everything you do day-to-day (create a payment, transition a status, fetch a gateway) should go through it rather than through repositories.PaymentMethodInterface — the contract every gateway (PayPal, Stripe, manual, etc.) implements. Reading one real implementation teaches you the bundle faster than any doc.Payment / PaymentMethod entities — check the status field and the amount field type before you build anything on top.First use case
Don't start with a real PSP. Write a ManualPaymentMethod (or "invoice") implementing PaymentMethodInterface that always returns an "accepted" or "pending" result. That gives you an end-to-end skeleton — create payment, persist, redirect, callback, transition status — without any network calls or credentials. Swap in a real gateway once the flow works.
Custom payment method as a tagged service
The idiomatic integration is not to fork the bundle; it's to add a class implementing PaymentMethodInterface and tag it so the registry picks it up:
App\Payment\InvoicePaymentMethod:
tags:
- { name: idci_payment.payment_method, alias: invoice }
Keep the class stateless and inject only an HTTP client plus config. Gateway credentials should come from your PaymentMethod configuration entity (or your own parameters), not from hardcoded constants — you'll want per-environment and per-currency gateways eventually.
Create → redirect → callback
The standard three-step flow:
Never trust the browser return URL
Treat the user-facing "return" route as cosmetic (show a spinner/receipt) and the server-to-server notify route as the source of truth. Pattern: on notify, re-fetch the payment by reference, ask the gateway for its authoritative status, then transition. This makes replay and forged returns harmless.
Status transitions, not status writes
Route every status change through the manager so the bundle can dispatch events and enforce allowed transitions. If you find yourself doing $payment->setStatus(...); $em->flush(); in a controller, you're bypassing the one thing the bundle actually gives you. If the built-in transition set doesn't cover your case (partial refunds, disputes), add your own subscriber and a small state map rather than editing the entity.
Testing
Because payment methods are plain services behind an interface, the whole flow is testable without HTTP: bind a fake PaymentMethodInterface returning a canned redirect, assert your listener transitions the payment and your order flips to "paid". For the real gateway, use its sandbox and a MockHttpClient for signature-verification paths — especially the failure paths (bad signature, duplicate notify, timeout).
composer.json, read the source before upgrading, and budget time for reading it up front. Vendor the diff if a release breaks you.Resources/config/services.xml — it tells you every service ID, tag name, and default argument in one file.resolve_target_entity or mapped superclasses, meaning you may need to configure your own Payment subclass in config/packages/doctrine.yaml to add fields. Do that before you have production data; retrofitting it later is a migration headache.access_control entry above your catch-all rule — a forgotten permitAll here will silently 302 the gateway's POST to your login page and payments will "randomly" never confirm.dump()-ing. Symfony's profiler shows the dispatched events under the Events panel — use it to confirm your subscriber actually fires before blaming the gateway.services.yaml so the bundle's own code calls your wrapper. Decoration is clean and upgrade-safe; editing vendor code is not.How can I help you explore Laravel packages today?