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

ezsystems/payment-core-bundle

JMSPaymentCoreBundle provides a unified Symfony API for integrating multiple payment backends. It handles payment and transaction persistence, retries, sensitive-data encryption, and supports many providers out of the box.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: this bundle is archived and targets Symfony 2.x/3.x (PHP 5.x–7.1 era). You should only adopt it to maintain or extend an existing app. For new work, the modern equivalents are Payum or league/omnipay. If you are on it, read the ReadTheDocs pages for the core bundle and whichever backend plugin you use (PayPal, AuthorizeNet, etc.) in parallel — the core API is thin, the backend plugin is where behaviour lives.

Install

composer require jms/payment-core-bundle
# plus one backend, e.g.
composer require jms/payment-paypal-bundle

Register in app/AppKernel.php:

new JMS\Payment\CoreBundle\JMSPaymentCoreBundle(),
new JMS\Payment\PaypalBundle\JMSPaymentPaypalBundle(),   // or your backend

Create your persisted entities. The bundle ships mapped superclasses; you are expected to subclass them so you control the table names, indexes and any extra columns:

// src/Acme/PaymentBundle/Entity/PaymentInstruction.php
namespace Acme\PaymentBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use JMS\Payment\CoreBundle\Entity\PaymentInstruction as BaseInstruction;

/**
 * @ORM\Entity
 * @ORM\Table(name="payment_instruction", indexes={@ORM\Index(columns={"created_at"})})
 */
class PaymentInstruction extends BaseInstruction
{
}

Do the same for Payment, Transaction, CreditCard (and StoredPaymentProfile/extended-data entities if you use recurring payments or extendedData), then point Doctrine at them so the bundle's classes resolve to yours:

# app/config/config.yml
doctrine:
    orm:
        resolve_target_entities:
            JMS\Payment\CoreBundle\Entity\PaymentInstruction: Acme\PaymentBundle\Entity\PaymentInstruction
            JMS\Payment\CoreBundle\Entity\Payment: Acme\PaymentBundle\Entity\Payment
            JMS\Payment\CoreBundle\Entity\Transaction: Acme\PaymentBundle\Entity\Transaction
            JMS\Payment\CoreBundle\Entity\CreditCard: Acme\PaymentBundle\Entity\CreditCard

First use case — "approve and deposit" after checkout. Everything goes through the plugin controller service, jms_payment_core.plugin_controller. It owns transaction creation, retry logic, and encryption, so never build Transaction objects by hand.

$ppc = $this->get('jms_payment_core.plugin_controller');

$instruction = $ppc->createPaymentInstruction(
    (new PaymentInstruction())
        ->setAmount(49.99)                        // numeric string, not float arithmetic
        ->setCurrency('EUR')
        ->setPaymentSystemName('paypal_express_checkout')
        ->setExtendedData(['return_url' => $returnUrl])
);

// Usually a redirect here (plugin returns a response/redirect object)
$response = $ppc->approveAndDeposit($instruction->getPayment()->getId(), 49.99);

if ($response->isRedirect()) {
    return $this->redirect($response->getRedirectUrl());
}

Then persist the instruction (it cascades to payment/transactions), store the instruction id in the session, and verify state on the return URL — see the flow in the docs' "Getting Started" example. Your first working end-to-end is: form → create instruction → approve/deposit → redirect → return controller → assert $payment->getState().

Implementation Patterns

Redirect-based checkout (the 90% case).

// checkoutAction: create + persist instruction, remember id in session
$ppc->createPaymentInstruction($instruction);
$em->persist($instruction);
$em->flush();

$response = $ppc->approveAndDeposit($instruction->getPayment()->getId(), $instruction->getAmount());

// completeAction (return URL): reload from DB, never trust query params
$instruction = $ppc->getPaymentInstruction($request->query->get('instructionId'));
$payment = $instruction->getPayment();

if ($payment->getState() === Payment::STATE_APPROVED
    && $payment->getApprovingAmount() == $instruction->getAmount()) {
    // mark order paid; idempotent — check your own order flag too
}

Note the plugin controller does not flush for you. Persist/flush after createPaymentInstruction, and flush again if you mutate state via the controller.

Resume the interrupted transaction. When the user abandons a redirect and comes back, don't create a second payment:

$pending = $ppc->getPendingTransaction($paymentId);
if ($pending) {
    // re-fetch/replay the pending approval, or surface "payment not completed"
}

Retries. Retry is built into the controller's transaction logic (it inspects previous failed/dependent transactions). Use $ppc->approve() / $ppc->deposit() separately when the backend supports split authorize+capture (auth at checkout, capture at shipment). Use reverseApproval() / reverseDeposit() / credit() for refunds rather than issuing a new payment.

Recurring / stored profiles. If the backend supports it, call createRecurringPaymentProfile() with a CreditCard (or stored profile) and persist the returned profile id against your customer. Subsequent charges reference that profile — the bundle will not manage scheduling for you; put that in a cron/command.

Backend selection. paymentSystemName is a string matching the backend's alias. Configure the backend bundle with credentials, and drive the value from a form (ChoosePaymentMethodType with a payment_systems option builds a radio/select of available systems for you). For card data use the bundled CreditCardType form; the CreditCard entity is what gets encrypted.

Write your own backend plugin when a gateway isn't covered:

class AcmePlugin extends AbstractPlugin implements PaymentPluginInterface
{
    public function approve(Transaction $transaction) { /* call API, throw on failure */ }
    public function deposit(Transaction $transaction) { /* ... */ }
    public function approveAndDeposit(Transaction $transaction) { /* ... */ }
    public function isIndependentCreditSupported() { return false; }
}

Tag it (jms_payment_core.plugin with an alias) so the plugin controller can route by paymentSystemName. Look at JMSPaymentPaypalBundle as the reference implementation — it is the best documentation for the interface's intended semantics.

Cross-cutting concerns: decorate, don't fork. Log every request/response, add audit trails, or normalise error messages by decorating jms_payment_core.plugin_controller in your services config and delegating to the inner service.

Testing. Mock the plugin controller in controller tests and assert your own order-state transitions; use a fake plugin (paymentSystemName: test) for functional tests so you never hit a gateway. The bundle's own test suite shows a NullPlugin-style setup worth copying.

Gotchas and Tips

  • Archived and unmaintained. No security patches. If you must use it, pin versions in composer.json, and treat any composer update as a review event. The encryption service in older tags depends on mcrypt (removed in PHP 7.2) — newer tags moved to OpenSSL. Verify which branch you installed, because the config keys and service ids differ between 1.3/1.4/master.
  • Encryption key changes are destructive. Encrypted card data/profile payloads become unreadable if you rotate the key or regenerate the app secret. Back up the key and write a re-encryption migration before rotating. Enabling encryption after data already exists also strands plaintext records.
  • Don't touch Payment::state directly. States (STATE_PENDING, STATE_APPROVED, dependent amounts, etc.) are driven by transaction results. Set them via the controller and always check both state and getApprovingAmount()/getDepositingAmount() — partial approvals are a real case with PayPal.
  • Flush ordering. Entities must have IDs before approveAndDeposit($paymentId, ...). Call persist() + flush() before invoking the plugin; otherwise you get "no payment with id" or duplicate-payment bugs on redirect.
  • Session dependency. getPaymentInstruction()/getPendingTransaction() rely on identifiers you provide; keep the instruction id in the session and in the return URL, and make the return action idempotent (browsers retry, users double-click).
  • Money is not a float. Amounts are numeric strings internally; use integer cents or bcmath in your own code, and assert $instruction->getCurrency() matches your order currency — mismatch is a silent gateway error.
  • extendedData is an untyped array column. Nothing validates it. Write your own constraints/pre-submit form listeners, and remember it round-trips through the backend, so don't stash objects there.
  • Every backend has its own quirks. PayPal Express vs. Payments Pro behave differently (redirect vs. direct card), test-mode URLs differ, and IPN/callback verification is not handled by the core bundle — implement it in your app and compare against $transaction->getResponse() / reason codes.
  • Debugging stuck payments: reload the PaymentInstruction with its Payment and all Transactions (getTransactions()), then inspect each transaction's state, type, reason code and raw response. Most "it did nothing" reports are a transaction stuck in STATE_PENDING after a lost return, or a failed preApproved dependent amount.
  • Add indexes on payment_instruction_id, state and created_at in your subclasses — the bundled superclasses ship no indexes, and the joins get slow quickly.
  • Extension points worth knowing: your mapped-subclass entities (add orderId, reference columns), a custom PaymentPluginInterface implementation, and service decoration of jms_payment_core.plugin_controller. Avoid editing anything under vendor/ — you will never upgrade again.
  • Migration path: the domain model (instruction → payment → transactions) maps fairly cleanly onto Payum's Payment/gateway concepts; extract your gateway calls behind a small interface now so swapping out the bundle later is a one-implementation change rather than a rewrite.
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.
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen