JMSPaymentCoreBundle

A unified API for processing payments with Symfony

Introduction

This bundle provides the foundation for different payment backends. It abstracts away the differences between payment protocols, and offers a simple, and unified API for performing financial transactions.

Features:

  • Simple, unified API (integrate once and use any payment provider)
  • Persistence of financial entities (such as payments, transactions, etc.)
  • Transaction management including retry logic
  • Encryption of sensitive data
  • Support for many payment backends out of the box
  • Easily support other payment backends

Getting started

Once you followed the Setup instructions, if you have no prior experience with this bundle or payment processing in general, you should follow the Accepting payments guide.

Once you grasp how this bundle works, take a look at the Payment form chapter to learn how to customize the form.

License

Setup

Installation

Install with composer:

composer require jms/payment-core-bundle

Configuration

The configuration is as simple as setting an encryption key which will be used for encrypting data. You can generate a random key with the following command:

bin/console jms_payment_core:generate-key

And then use it in your configuration:

# config/packages/payment.yaml
jms_payment_core:
    encryption:
        secret: output_of_above_command

Warning

If you change the secret or the crypto provider, all encrypted data will become unreadable.

Create database tables

This bundle requires a few database tables, which you can create as follows.

If you’re not using database migrations:

bin/console doctrine:schema:update

Or, if you’re using migrations:

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate

Note

It’s assumed you have entity auto mapping enabled, which is usually the case. If you don’t, you need to either enable it:

# config/packages/doctrine.yaml
doctrine:
    orm:
        auto_mapping: true

Or explicitly register the configuration from this bundle:

# config/packages/doctrine.yaml
doctrine:
    orm:
        mappings:
            JMSPaymentCoreBundle: ~

Configure a payment backend

In addition to setting up this bundle, you will also need to install a plugin for each payment backend you intend to support. Plugins are simply bundles you add to your application, as you would with any other Symfony bundle.

Tip

See Available payment backends for the list of existing plugins.

Using the Paypal plugin as an example, you would install it with composer:

composer require jms/payment-paypal-bundle

And configure it:

# config/packages/payment.yaml

jms_payment_paypal:
    username: your api username
    password: your api password
    signature: your api signature

Note

Other plugins will require different configuration. Take a look at their documentation for complete instructions.

Next steps

If you have no prior experience with this bundle or payment processing in general, you should follow the Accepting payments guide. Otherwise, proceed to the Payment form chapter.

Payment form

This bundle ships with a form type that automatically renders a choice (radio button, select) so that the user can choose their preferred payment method.

Additionally, each payment plugin you have installed, includes a specific form that is also rendered. This form is dependent on the payment method itself, different methods will have different forms.

As an example, if you have both the PayPal and Paymill plugins installed, both their forms will be rendered. In PayPal’s case, the form is empty (since the user does not enter any information on your site) but for Paymill a Credit Card form is rendered.

Tip

See the Accepting payments guide for detailed instructions on how to integrate the form in your application, namely how to handle form submission.

Creating the form

When creating the form you need to specify at least the amount and currency options. See below for all the available options.

// src/App/Controller/OrdersController.php

use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType;

$form = $this->createForm(ChoosePaymentMethodType::class, null, [
    'amount'   => '10.42',
    'currency' => 'EUR',
]);

Note

If your Symfony version is earlier than 3.0, you must refer to the form by its alias instead of using the class directly:

// src/App/Controller/OrdersController.php

$form = $this->createForm('jms_choose_payment_method', null, [
    'amount'   => '10.42',
    'currency' => 'EUR',
]);

Changing how the form looks

If you need to change how the form looks, you can use form theming, which allows you to customize how each element of the form is rendered. Our theme will be implemented in a separate Twig file, which we will then reference from the template where the form is rendered.

Tip

See the form component’s documentation for more information about form theming

Start by creating an empty theme file:

{# templates/Orders/theme.html.twig #}

{% extends 'form_div_layout.html.twig' %}

Note

We’re extending Symfony’s default form_div_layout.html.twig theme. If your application is setup to use another theme, you probably want to extend that one instead.

And then reference it from the template where the form is rendered:

{# templates/Orders/show.html.twig #}

{% form_theme form 'Orders\theme.html.twig' %}

{{ form_start(form) }}
    {{ form_widget(form) }}
    <input type="submit" value="Pay € {{ order.amount }}" />
{{ form_end(form) }}
Hiding the payment method radio button

When the form only has one available payment method (either because only one payment plugin is installed or because you used the allowed_methods option) you likely want to hide the payment method radio button completely. You can do so as follows:

{# templates/Orders/theme.html.twig #}

{# Don't render the radio button's label #}
{% block _jms_choose_payment_method_method_label %}
{% endblock %}

{# Hide each entry in the radio button #}
{% block _jms_choose_payment_method_method_widget %}
    <div style="display: none;">
        {{ parent() }}
    </div>
{% endblock %}

Tip

If you hide the radio button, you will want to use the default_method option to automatically select the payment method.

Available options

amount

Mandatory

The amount (i.e. total price) of the payment.

// src/App/Controller/OrdersController.php

use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType;

$form = $this->createForm(ChoosePaymentMethodType::class, null, [
    'amount'   => '10.42',
    'currency' => 'EUR',
]);

You might want to add extra costs for a specific payment method. You can implement this by passing a closure instead of a static value:

// src/App/Controller/OrdersController.php

use JMS\Payment\CoreBundle\Entity\ExtendedData;
use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType;

$amount = '10.42';

$amountClosure = function ($currency, $paymentSystemName, ExtendedData $data) use ($amount) {
    if ($paymentSystemName === 'paypal_express_checkout') {
        return $amount * 1.05;
    }

    return $amount;
};

$form = $this->createForm(ChoosePaymentMethodType::class, null, [
    'amount'   => $amountClosure,
    'currency' => 'EUR',
]);
currency

Mandatory

The three-letter currency code, i.e. EUR or USD.

// src/App/Controller/OrdersController.php

use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType;

$form = $this->createForm(ChoosePaymentMethodType::class, null, [
    'amount'   => '10.42',
    'currency' => 'EUR',
]);
predefined_data

Optional

Default: []

The payment plugins likely require you to provide additional configuration in order to create a payment. You can do this by passing an array to the predefined_data option of the form.

As an example, if we would be using the Stripe plugin, we would need to provide a description, which would look like the following:

// src/App/Controller/OrdersController.php

use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType;

$predefinedData = [
    'stripe_checkout' => [
        'description' => 'My product',
    ],
];

$form = $this->createForm(ChoosePaymentMethodType::class, null, [
    'amount'          => '10.42',
    'currency'        => 'EUR',
    'predefined_data' => $predefinedData,
]);

If you would be using multiple payment backends, the $predefinedData array would have an entry for each of the methods:

// src/App/Controller/OrdersController.php

$predefinedData = [
    'paypal_express_checkout' => [...],
    'stripe_checkout'         => [...],
];
allowed_methods

Optional

Default: []

In case you wish to constrain the methods presented to the user, use the allowed_methods option:

// src/App/Controller/OrdersController.php

use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType;

$form = $this->createForm(ChoosePaymentMethodType::class, null, [
    'amount'          => '10.42',
    'currency'        => 'EUR',
    'allowed_methods' => ['paypal_express_checkout']
]);
default_method

Optional

Default: null

By default, no payment method is selected in the radio button, which means users must select one themselves. This is the case even if you only have one payment method available.

If you wish to set a default payment method, you can use the default_method option:

// src/App/Controller/OrdersController.php

use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType;

$form = $this->createForm(ChoosePaymentMethodType::class, null, [
    'amount'          => '10.42',
    'currency'        => 'EUR',
    'default_method'  => 'paypal_express_checkout',
]);
choice_options

Optional

Default: []

Pass options to the payment method choice type. See the ChoiceType refererence for all available options.

For example, to display a select instead of a radio button, set the expanded option to false:

// src/App/Controller/OrdersController.php

use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType;

$form = $this->createForm(ChoosePaymentMethodType::class, null, [
    'amount'         => '10.42',
    'currency'       => 'EUR',
    'choice_options' => [
        'expanded' => false,
    ],
]);
method_options

Optional

Default: []

Pass options to each payment method’s form type. For example, to hide the main label of the PayPal Express Checkout form, set the label option to false:

// src/App/Controller/OrdersController.php

use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType;

$form = $this->createForm(ChoosePaymentMethodType::class, null, [
    'amount'         => '10.42',
    'currency'       => 'EUR',
    'method_options' => [
        'paypal_express_checkout' => [
            'label' => false,
        ],
    ],
]);

Events

The PluginController dispatches events for certain payment changes. This can be used by your application to perform certain actions, for example, when a payment is successful.

Take a look at Symfony’s documentation for information on how to listen to events.

PaymentInstruction State Change Event

Name: payment_instruction.state_change

Class: JMS\Payment\CoreBundle\PluginController\Event\PaymentInstructionStateChangeEvent

This event is dispatched after the state of a payment instruction changes.

You have access to the PaymentInstruction, the new state and the old state of the payment instruction.

Payment State Change Event

Name: payment.state_change

Class: JMS\Payment\CoreBundle\PluginController\Event\PaymentStateChangeEvent

This event is dispatched directly after the state of a payment changed. All related entities have already been updated.

You have access to the Payment, the PaymentInstruction, the new state and the old state of the payment.

Plugins

A plugin is a flexible way of providing access to a specific payment back end, payment processor, or payment service provider. Plugins are used to execute a FinancialTransaction against a payment service provider, such as Paypal.

Implementing a custom plugin

The easiest way is to simply extend the provided AbstractPlugin class, and override the remaining abstract methods:

use JMS\Payment\CoreBundle\Plugin\AbstractPlugin;

class PaypalPlugin extends AbstractPlugin
{
    public function processes($name)
    {
        return 'paypal' === $name;
    }
}

Now, you only need to setup your plugin as a service, and it will be added to the plugin controller automatically:

  • YAML
    services:
        payment.plugin.paypal:
            class: PaypalPlugin
            tags: [{name: payment.plugin}]
    
  • XML
    <service id="payment.plugin.paypal" class="PaypalPlugin">
        <tag name="payment.plugin" />
    </service>
    

That’s it! You just created your first plugin :) Right now, it does not do anything useful, but we will get to the specific transactions that you can perform in the next section.

Available transaction types

Each plugin may implement a variety of available transaction types. Depending on the used payment method and the capabilities of the backend, you rarely need all of them.

Following is a list of all available transactions, and two exemplary payment method plugins. A “x” indicates that the method is implement, “-” that it is not:

Financial Transaction CreditCardPlugin ElectronicCheckPlugin
checkPaymentInstruction x x
validatePaymentInstruction x x
approveAndDeposit x x
approve x -
reverseApproval x -
deposit x x
reverseDeposit x -
credit x -
reverseCredit x -

If you are unsure which transactions to implement, have a look at the PluginInterface which contains detailed descriptions for each of them.

Tip

In cases, where a certain method does not make sense for your payment backend, you should throw a FunctionNotSupportedException. If you extend the AbstractPlugin base class, this is already done for you.

Available exceptions

Exceptions play an important part in the communication between the different payment plugin, and the PluginController which manages them.

Following is a list with available exceptions, and how they are treated by the PluginController. Of course, you can also add your own exceptions, but it is recommend that you sub-class an existing exception when doing so.

Tip

All exceptions which are relevant for plugins are located in the namespace JMS\Payment\CoreBundle\Plugin\Exception.

Class Description Payment Plugin Controller Interpretation
Exception Base exception used by all exceptions thrown from plugins. Causes any transaction to be rolled back. Exception will be re-thrown.
FunctionNotSupportedException This exception is thrown whenever a method on the interface is not supported by the plugin. In most cases, this causes any transactions to be rolled back. Notable exceptions to this rule: checkPaymentInstruction, validatePaymentInstruction
InvalidDataException This exception is thrown whenever the plugin realizes that the data associated with the transaction is invalid. Causes any transaction to be rolled back. Exception will be re-thrown.
InvalidPaymentInstructionException This exception is typically thrown from within either checkPaymentInstruction, or validatePaymentInstruction. Causes PaymentInstruction to be set to STATE_INVALID.
BlockedException

This exception is thrown whenever a transaction cannot be processed.

The exception must only be used when the situation is temporary, and there is a chance that the transaction can be performed at a later time successfully.

Sets the transaction to STATE_PENDING, and converts the exception to a Result object.
TimeoutException (sub-class of BlockedException) This exception is thrown when there is an enduring communication problem with the payment backend system. Sets the transaction to STATE_PENDING, and converts the exception to a Result object.
ActionRequiredException (sub-class of BlockedException)

This exception is thrown whenever an action is required before the transaction can be completed successfully.

A typical action would be for the user to visit an URL in order to authorize the payment.

Sets the transaction to STATE_PENDING, and converts the exception to a Result object.

Model

PaymentInstruction

A PaymentInstruction is the first object that you need to create. It contains information such as the total amount, the payment method, the currency, and any data that is necessary for the payment method, for example credit card information.

Tip

Any payment related data may be automatically encrypted if you request this.

Below you find the different states that a PaymentInstruction can go through:

digraph PaymentInstruction_State_Flow {
"New" -> "Valid";
"New" -> "Invalid";
"Valid" -> "Closed";
}

Payment

Each PaymentInstruction may be split up into several payments. A Payment always holds an amount, and the current state of the workflow, such as initiated, approved, deposited, etc.

This allows, for example, to request a fraction of the total amount to be deposited before an order ships, and the rest afterwards.

Below, you find the different states that a Payment can go through:

digraph Payment_State_Flow {
"New" -> "Canceled"
"New" -> "Approving"

"Approving" -> "Approved"
"Approving" -> "Failed"

"Approved" -> "Depositing"

"Depositing" -> "Deposited"
"Depositing" -> "Expired"
"Depositing" -> "Failed"
}

FinancialTransaction

Each Payment may have several transactions. Each FinancialTransaction represents a specific interaction with the payment backend. In the case of a credit card payment, this could for example be an authorization transaction.

Note

There may only ever be one open transaction for each PaymentInstruction at a time. This is enforced, and guaranteed.

Below, you find the different states that a FinancialTransaction can go through:

digraph Financial_Transaction_State_Flow {
"New" -> "Pending"
"New" -> "Failed"
"New" -> "Success"
"New" -> "Canceled"

"Pending" -> "Failed"
"Pending" -> "Success"
}

Available payment backends

This is the list of currently supported payment backends, through community-created plugins.

Note

If a backend you intend to use is not on the list, you can create a custom plugin.

Tip

If you have implemented a payment backend, please add it to this list by editing this file on GitHub and submitting a Pull Request.

Accepting payments

In this guide, we explore how to accept payments using this bundle, by building a simplified Checkout system from scratch.

Tip

In no way are you forced to use the presented system in your application, this is merely the simplest way to show this bundle in action. We recomend you follow the steps below and, once you grasp how this bundle works, think about the best way to integrate it into your application.

Warning

We have completely left out any security considerations. In a real-world scenario, you must make sure a user is not able to access other users’ data.

The Order entity

The Order entity represents what is being purchased and usually contains:

  • $id: The unique id of the order
  • $amount: The total price
  • $paymentInstruction: The PaymentInstruction instance

Tip

If you’re wondering what a PaymentInstruction is, take a look at The Model, though you don’t strictly need to understand it to follow the instructions below.

Here’s the full code for a minimal Order entity:

// src/App/Entity/Order.php

namespace App\Entity;

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

/**
 * @ORM\Table(name="orders")
 * @ORM\Entity
 */
class Order
{
    /**
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\OneToOne(targetEntity="JMS\Payment\CoreBundle\Entity\PaymentInstruction")
     */
    private $paymentInstruction;

    /**
     * @ORM\Column(type="decimal", precision=10, scale=5)
     */
    private $amount;

    public function __construct($amount)
    {
        $this->amount = $amount;
    }

    public function getId()
    {
        return $this->id;
    }

    public function getAmount()
    {
        return $this->amount;
    }

    public function getPaymentInstruction()
    {
        return $this->paymentInstruction;
    }

    public function setPaymentInstruction(PaymentInstruction $instruction)
    {
        $this->paymentInstruction = $instruction;
    }
}

Warning

Note that the precision and scale in the $amount column definition are set to 10 and 5, respectively. This is consistent with the mapping this bundle uses internally and means that the greatest amount you will be able to accept is 99999.99999.

See the Overriding entity mapping guide for instructions on how to override this limit.

Before proceeding, make sure you update your database schema, in order to create the orders table:

bin/console doctrine:schema:update

Or, if using migrations:

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate

The Controller

Each step of our Checkout process will be implemented as an action in an OrdersController. All routes will be namespaced under /orders.

Go ahead and create the controller:

// src/App/Controller/OrdersController.php

namespace App\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

/**
 * @Route("/orders")
 */
class OrdersController extends AbstractController
{
}

Creating an Order

The first step in our Checkout process is to create an Order, which we will do in a newAction. This action acts as the bridge between the Checkout process and the rest of your application.

To simplify, we will only be passing an amount (the total price of the items being purchased) as a parameter to the action. In a real world application you would probably pass the $id of a Shopping Cart, or a similar entity that holds information about the items being purchased.

Create the newAction in the OrdersController:

// src/App/Controller/OrdersController.php

use AppBundle\Entity\Order;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/new/{amount}")
 */
public function newAction($amount)
{
    $em = $this->getDoctrine()->getManager();

    $order = new Order($amount);
    $em->persist($order);
    $em->flush();

    return $this->redirectToRoute('app_orders_show', [
        'orderId' => $order->getId(),
    ]);
}

If you navigate to /orders/new/42.24, a new Order will be inserted in the database with 42.24 as the amount and you will be redirected to the showAction, which we will create next.

Creating the payment form

Once the Order has been created, the next step in our Checkout process is to display it, along with the payment form. We will be doing this in a showAction:

// src/App/Controller/OrdersController.php

use App\Entity\Order;
use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;

/**
 * @Route("/{orderId}/show")
 */
public function showAction($orderId, Request $request, PluginController $ppc)
{
    $order = $this->getDoctrine()->getManager()->getRepository(Order::class)->find($orderId);

    $form = $this->createForm(ChoosePaymentMethodType::class, null, [
        'amount'   => $order->getAmount(),
        'currency' => 'EUR',
    ]);

    return $this->render('Orders/show.html.twig', [
        'order' => $order,
        'form'  => $form->createView(),
    ]);
}

Note

If your Symfony version is earlier than 3.0, you must refer to the form by its alias instead of using the class directly:

// src/AppBundle/Controller/OrdersController.php

$form = $this->createForm('jms_choose_payment_method', null, [
    'amount'   => $order->getAmount(),
    'currency' => 'EUR',
]);

And the corresponding template:

{# templates/Orders/show.html.twig #}

Total price: € {{ order.amount }}

{{ form_start(form) }}
    {{ form_widget(form) }}
    <input type="submit" value="Pay € {{ order.amount }}" />
{{ form_end(form) }}

If you now refresh the page in your browser, you should see the template rendered, with all the payment methods you have installed. The form includes a radio button so the user can select the payment method they wish to use.

Tip

If you get a There is no payment method available exception, you haven’t configured any payment backends yet. Please see Configure a payment backend for information on how to do this.

Tip

See Payment form for information on all the available options you can pass to the form.

Handling form submission

We’ll handle form submission in the same action which renders the form. Upon binding, the form type will validate the data for the chosen payment method and, on success, give us back a valid PaymentInstruction instance.

We’ll attach this PaymentInstruction to the Order and then redirect to the paymentCreateAction. In case the form is not valid, we don’t redirect and the template is re-rendered with form errors displayed.

Note that no remote calls to the payment backend are made in this action, we’re simply manipulating data in the local database.

// src/App/Controller/OrdersController.php

use App\Entity\Order;
use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;

/**
 * @Route("/{orderId}/show")
 */
public function showAction($orderId, Request $request, PluginController $ppc)
{
    $form = $this->createForm(ChoosePaymentMethodType::class, null, [
        'amount'   => $order->getAmount(),
        'currency' => 'EUR',
    ]);

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $ppc->createPaymentInstruction($instruction = $form->getData());

        $order->setPaymentInstruction($instruction);

        $em = $this->getDoctrine()->getManager();
        $em->persist($order);
        $em->flush($order);

        return $this->redirectToRoute('app_orders_paymentcreate', [
            'orderId' => $order->getId(),
        ]);
    }

    return $this->render('Orders/show.html.twig', [
        'order' => $order,
        'form'  => $form->createView(),
    ]);
}

Depositing money

In the previous section, we created our PaymentInstruction and redirected to the paymentCreateAction. In this section we will be implementing that action.

Creating a Payment instance

Let’s start by creating a private method in our controller, which will aid us in creating the Payment instance. No remote calls will be made yet.

// src/App/Controller/OrdersController.php

use App\Entity\Order;
use JMS\Payment\CoreBundle\PluginController\PluginController;

private function createPayment(Order $order, PluginController $ppc)
{
    $instruction = $order->getPaymentInstruction();
    $pendingTransaction = $instruction->getPendingTransaction();

    if ($pendingTransaction !== null) {
        return $pendingTransaction->getPayment();
    }

    $amount = $instruction->getAmount() - $instruction->getDepositedAmount();

    return $ppc->createPayment($instruction->getId(), $amount);
}
Issuing the payment

Now we’ll call the createPayment method we implemented in the previous section in a new createPaymentAction, where we will actually create a payment through the payment backend and, if successful, redirect the user to a paymentCompleteAction:

// src/App/Controller/OrdersController.php

use App\Entity\Order;
use Symfony\Component\Routing\Annotation\Route;
use JMS\Payment\CoreBundle\PluginController\PluginController;
use JMS\Payment\CoreBundle\PluginController\Result;

/**
 * @Route("/{orderId}/payment/create")
 */
public function paymentCreateAction($orderId, PluginController $ppc)
{
    $order = $this->getDoctrine()->getManager()->getRepository(Order::class)->find($orderId);

    $payment = $this->createPayment($order, $ppc);

    $result = $ppc->approveAndDeposit($payment->getId(), $payment->getTargetAmount());

    if ($result->getStatus() === Result::STATUS_SUCCESS) {
        return $this->redirectToRoute('app_orders_paymentcomplete', [
            'orderId' => $order->getId(),
        ]);
    }

    throw $result->getPluginException();

    // In a real-world application you wouldn't throw the exception. You would,
    // for example, redirect to the showAction with a flash message informing
    // the user that the payment was not successful.
}

Tip

If you get an Unable to generate a URL exception, the transaction was successful. We just haven’t created that action yet, we will be doing so later.

If you get an ActionRequiredException, you are using a payment backend which requires offsite operations. In the next section we explain what this means and how to support it.

Performing the payment offsite

Certain payment backends (e.g. Paypal) require the user to go their site to actually perform the payment. In that case, $result will have status Pending and we need to redirect the user to a given URL.

We would add the following to our action:

// src/App/Controller/OrdersController.php

use JMS\Payment\CoreBundle\Plugin\Exception\Action\VisitUrl;
use JMS\Payment\CoreBundle\Plugin\Exception\ActionRequiredException;
use JMS\Payment\CoreBundle\PluginController\Result;

if ($result->getStatus() === Result::STATUS_PENDING) {
    $ex = $result->getPluginException();

    if ($ex instanceof ActionRequiredException) {
        $action = $ex->getAction();

        if ($action instanceof VisitUrl) {
            return $this->redirect($action->getUrl());
        }
    }
}

throw $result->getPluginException();

Tip

If you get an exception, you probably didn’t configure the payment plugin correctly. Take a look at the respective plugin’s documentation and make sure you followed the instructions.

Displaying a Payment complete page

The last step in out Checkout process is to tell the user the payment was successful. We wil be doing so in a paymentCompleteAction, to which we have been redirected from the paymentCreateAction:

// src/App/Controller/OrdersController.php

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/{orderId}/payment/complete")
 */
public function paymentCompleteAction($orderId)
{
    return new Response('Payment complete');
}

Migrating from Mcrypt

Coming soon

Overriding entity mapping

By default, this bundle sets the type of database columns which store amounts to decimal with the precision set to 10 and scale set to 5. This means that the greatest amount you are able to process is 99999.99999.

In case you need to accept payments of greater value, it’s possible to override the entity mapping supplied by this bundle and use a custom one. Keep reading for instructions on how to do this.

Note

In a future major release, amounts will be stored as strings, thus removing this limitation.

Copying the mapping files

Start by copying the mapping files from this bundle to your application:

cd my-app
mkdir -p config/packages/JMSPaymentCoreBundle
cp vendor/jms/payment-core-bundle/JMS/Payment/CoreBundle/Resources/config/doctrine/* config/packages/JMSPaymentCoreBundle/

You now have a copy of the following mapping files under config/packages/JMSPaymentCoreBundle:

  • Credit.orm.xml
  • FinancialTransaction.orm.xml
  • Payment.orm.xml
  • PaymentInstruction.orm.xml

Configuring custom mapping

The next step is to tell Symfony to use your copy of the files instead of the ones supplied by this bundle:

# config/packages/doctrine.yml

doctrine:
    orm:
        # ...
        mappings:
            JMSPaymentCoreBundle:
                type: xml
                dir: '%kernel.root_dir%/config/packages/JMSPaymentCoreBundle'
                prefix: JMS\Payment\CoreBundle\Entity
                alias: JMSPaymentCoreBundle

Overriding decimal columns

Symfony is now using your custom mapping. Taking PaymentInstruction.orm.xml as an example, we can increase the maximum value of the amount column as follows:

<!-- config/packages/JMSPaymentCoreBundle/PaymentInstruction.orm.xml -->

<!-- Set maximum value to 9999999999.99999 -->
<field name="amount" type="decimal" precision="15" scale="5" />

Warning

Make sure you change the definition of all the decimal columns in all the mapping files.

Updating the database

Now that you changed the mapping, you need to update your database schema.

If you’re not using database migrations:

bin/console doctrine:schema:update

Or, if you’re using migrations:

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate