Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Payment Core Bundle Laravel Package

paymentsuite/payment-core-bundle

Core payment bundle for PaymentSuite that provides shared abstractions, payment method handling, and integration points for PHP payment workflows. It centralizes common payment logic so gateways and checkout flows can plug in consistently, avoid duplicated code, and stay modular and reusable.

View on GitHub
Deep Wiki
Context7

Getting Started

What you're installing. paymentsuite/payment-core-bundle is not a payment gateway. It's the shared domain layer of the PaymentSuite family: payment-method abstraction, order bridging, the payment dispatcher/events, and the DI plumbing that lets gateway bundles (payments-redsys, payments-stripe, payments-paypal, …) plug in. You install it once, then install one or more method bundles on top. If you're not using (or maintaining) a PaymentSuite-based app, you don't want this package — see Gotchas.

Prerequisites. Symfony 2.3–2.8-era skeleton (AppKernel, app/config/config.yml), PHP 5.x-compatible runtime, Composer. There is no Symfony 3/4/5 support in the 2014 release line; expect a fork-and-patch job if you need to run it on anything modern.

Install and register.

composer require paymentsuite/payment-core-bundle
// app/AppKernel.php
$bundles = [
    // ...
    new PaymentSuite\PaymentCoreBundle\PaymentCoreBundle(),
    // gateway bundles go after it
    new PaymentSuite\Payments\RedSysBundle\PaymentsRedSysBundle(),
];
# app/config/config.yml
payment_core:
    currency: EUR

The bundle itself ships no routes — the HTTP flow (execute/callback controllers) lives in PaymentWebBundle, and each gateway bundle ships its own routing. Import only what you install.

Where to look first, in this order:

  1. PaymentSuite\PaymentCoreBundle\PaymentMethodInterface — the contract every gateway implements (getPaymentName(), option getters/setters).
  2. PaymentSuite\PaymentCoreBundle\PaymentMethodWrapper — the object you actually call setOrder()/setAmount()/setCurrency() on before processing.
  3. Order\OrderBridgeInterface and Order\PaymentBridgeInterface — the two interfaces your app's entities must implement.
  4. Services\PaymentMethodManager (loadPaymentMethod()) and Services\PaymentDispatcher (fires PaymentEvents::PAYMENT_SUCCESS / PAYMENT_FAILED / PAYMENT_PREPARED).
  5. DependencyInjection\Compiler\RegisterPaymentMethodsPass — explains why a gateway that "isn't there" really isn't registered.

First end-to-end use case. Wire your Order entity to the core, load a method by name, hand it the order and amount, dispatch it:

// src/AppBundle/Entity/Order.php
use PaymentSuite\PaymentCoreBundle\Order\OrderBridgeInterface;
use PaymentSuite\PaymentCoreBundle\Order\PaymentBridgeInterface;

class Order implements OrderBridgeInterface
{
    /** @var Payment */
    private $payment;

    public function getPaymentBridge(): PaymentBridgeInterface
    {
        return $this->payment;
    }

    public function setPaymentBridge(PaymentBridgeInterface $payment)
    {
        $this->payment = $payment;
        return $this;
    }
}
// in a controller or your own CheckoutService
use PaymentSuite\PaymentCoreBundle\Exception\PaymentException;

$method = $this->get('paymentsuite.payment.method.manager')
    ->loadPaymentMethod('redsys');            // alias registered by the gateway bundle

$method
    ->setOrder($order)
    ->setAmount(1999)                          // minor units — 19.99 EUR
    ->setCurrency('EUR');

try {
    $this->get('paymentsuite.payment.manager')->processPayment($method);
} catch (PaymentException $e) {
    // gateway rejected / misconfigured
}

processPayment() produces the gateway response (usually a redirect or a form). The gateway's own callback controller later re-enters the stack and fires the success/failure event, which is where your order actually gets marked paid.

Implementation Patterns

Bridge, don't inherit. PaymentCore never touches your Order directly — it goes through OrderBridgeInterface::getPaymentBridge(). Keep the bridge as a thin, dumb entity (Payment with transaction_id, amount, status, method, raw_response). No business logic there; it's ORM-coupled and painful to unit test.

Method-agnostic application code. Everything above the facade should depend only on PaymentMethodInterface. In practice you write one small wrapper service and never touch PaymentCore from controllers again:

class PaymentProcessor
{
    public function __construct($methodManager, $paymentManager, $orderManager) { ... }

    public function start(Order $order, string $gateway): PaymentMethodInterface
    {
        $method = $this->methodManager->loadPaymentMethod($gateway);
        $method->setOrder($order)->setAmount($order->getTotalCents())->setCurrency('EUR');

        $this->paymentManager->processPayment($method);

        return $method;
    }
}

Swapping RedSys for Stripe then becomes a config change plus one bundle, not a refactor.

Event listeners are where the domain happens. The core is deliberately silent about what "paid" means. Register listeners and keep them idempotent:

// services.yml
app.payment_listener:
    class: AppBundle\EventListener\PaymentListener
    tags:
        - { name: kernel.event_listener, event: payment.success, method: onSuccess }
        - { name: kernel.event_listener, event: payment.failed,  method: onFailed }

// PaymentListener
public function onSuccess(PaymentEvent $event)
{
    $order = $this->orderManager->getOrder();   // or pull from the event's bridge
    if ($order->isPaid()) {
        return;                                  // IPN replays are normal
    }
    $order->setPaidAt(new \DateTime());
    $this->orderManager->orderPaid($order);
    $this->invoiceService->generate($order);
    $this->mailer->sendConfirmation($order);
}

Use the same listener for stock release, coupon consumption, and analytics. One listener per concern, all on the same event, keeps them independently testable.

Selection at runtime. loadPaymentMethod($alias) is keyed by the tag alias each gateway bundle registers. Build a config-driven allowlist (payment_methods: [redsys, stripe]) and validate against $methodManager->getAvailableMethods() (or by catching the load exception) rather than letting users post arbitrary aliases.

Testing. Don't boot gateways in tests. Either build a fake PaymentMethodInterface implementation, or register a stub service under the paymentsuite.payment_method tag with your test alias. Test listeners by constructing PaymentEvent with a mocked wrapper — they're plain PHP objects, no container needed.

Integration tips.

  • Keep a Payment row per attempt, not per order; you want an audit trail when a customer retries with a different card or gateway.
  • Persist the order before processPayment() — most gateway callbacks arrive out-of-band with no session.
  • Store the gateway's raw response JSON in the bridge; you will need it for chargebacks and support tickets.
  • Put the facade behind an interface so a future Payum/Symfony Workflow rewrite can replace PaymentCore without touching checkout code.

Gotchas and Tips

It is abandoned. Last release April 2014, 25 stars, zero dependents, Symfony 2 only. There are no upstream security patches, no PHP 8 compatibility (old DI conventions, no return types, @expectedException-era tests), and no PSR-4/composer.json cleanup done for you. Practical stance: vendor-fork it, pin the fork, and budget a migration. For new projects use Payum, symfony/workflow + a PSP SDK, or Stripe/Adyen's own libraries.

Service IDs are version-dependent. Documentation and blog posts disagree on names (paymentsuite.payment.manager vs payment.manager, payment.method.manager vs paymentsuite.payment.method.manager). Never trust a snippet — confirm with php app/console container:debug | grep -i payment (Symfony 2.x) and read the bundle's Resources/config/services.yml.

"Method not found" usually means the compiler pass didn't see it. RegisterPaymentMethodsPass collects services tagged paymentsuite.payment_method. If loadPaymentMethod('foo') throws, check three things in order: the gateway bundle is registered in AppKernel after PaymentCoreBundle; the tag has the correct alias attribute; and the gateway's own extension was loaded (its config block must be present in config.yml, even if empty).

Events are not guaranteed by the core. PaymentDispatcher only fires what the concrete gateway calls. Some gateway bundles dispatch on the callback controller, some inside processPayment(), some only in their success controller. Read the gateway bundle's code — a missing payment.success dispatch is the classic "order stays pending forever" bug.

Double-dispatch on IPN/webhook endpoints. Gateways retry notifications. There's no idempotency layer in PaymentCore; you must guard in your listener (unique index on transaction_id, or an isPaid() early return). Test this by invoking the callback URL twice.

Currency lives in payment_core, amount lives in the wrapper. They're set independently, and a mismatch (currency: EUR config, setCurrency('USD') on the wrapper) will surface as an opaque gateway error, not a validation error. Pick one source of truth — config — and don't pass currency per call.

Amount units are minor units. setAmount(1999) means 19.99. Passing a float from $order->getTotal() silently produces 100x charges. Keep a getTotalCents(): int on your order.

OrderManager may be session-backed. Several 1.x builds keep the current order in the session rather than re-fetching by ID. That breaks in multi-tab checkouts, after session GC, and in headless callback flows. If you hit it, don't fight it — resolve the order from your own repository in the listener via the transaction ID on the bridge.

Bridges are read by listeners after the fact. getPaymentBridge() must return a persisted entity before the success event fires, or invoice generation and order updates will operate on nulls. Flush after processPayment(), not before the listener runs.

Debugging. The core logs almost nothing. Add one listener per event that logs paymentName, amount, order ID, and a correlation ID; that single addition cuts most PaymentSuite support time by half. Enable the Symfony profiler around the callback to inspect the request the gateway actually sent.

Extension points worth knowing. Implement PaymentMethodInterface + a tagged factory service for an in-house gateway; decorate PaymentMethodManager to add logging or method allowlisting; listen to PAYMENT_PREPARED if you need to snapshot the order before it's sent to the PSP.

Don't put logic in the bridge entities and don't let controllers call PaymentDispatcher directly — always go through the gateway's flow so event ordering stays consistent with what the gateway bundle expects.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console