How to integrate a Payment Gateway as a Plugin?¶
Among all possible customizations, new gateway provider is one of the most common choices. Payment processing complexity, regional limits and the amount of potential payment providers makes it hard for Sylius core to keep up with all possible cases. A custom payment gateway is sometimes the only choice.
In the following example, a new gateway will be configured, which will send payment details to external API.
1. Set up a new plugin using the PluginSkeleton.
composer create-project sylius/plugin-skeleton ProjectName
2. The first step in the newly created repository would be to create a new Gateway Factory.
Prepare a gateway factory class in
src/Payum/SyliusPaymentGatewayFactory.php
:// src/Payum/SyliusPaymentGatewayFactory.php <?php declare(strict_types=1); namespace Acme\SyliusExamplePlugin\Payum; use Payum\Core\Bridge\Spl\ArrayObject; use Payum\Core\GatewayFactory; final class SyliusPaymentGatewayFactory extends GatewayFactory { protected function populateConfig(ArrayObject $config): void { $config->defaults([ 'payum.factory_name' => 'sylius_payment', 'payum.factory_title' => 'Sylius Payment', ]); } }And at the end of
src/Resources/config/services.xml
orsrc/Resources/config/services.yaml
add such a configuration for your gateway:<!-- src/Resources/config/services.xml --> <service id="app.sylius_payment" class="Payum\Core\Bridge\Symfony\Builder\GatewayFactoryBuilder"> <argument>Acme\SyliusExamplePlugin\Payum\SyliusPaymentGatewayFactory</argument> <tag name="payum.gateway_factory_builder" factory="sylius_payment" /> </service># src/Resources/config/services.yaml app.sylius_payment: class: Payum\Core\Bridge\Symfony\Builder\GatewayFactoryBuilder arguments: [ Acme\SyliusExamplePlugin\Payum\SyliusPaymentGatewayFactory ] tags: - { name: payum.gateway_factory_builder, factory: sylius_payment }
3. Next, one should create a configuration form, where authorization (or some additional information, like sandbox mode) can be specified.
Create the configuration type in
src/Form/Type/SyliusGatewayConfigurationType.php
:// src/Form/Type/SyliusGatewayConfigurationType.php <?php declare(strict_types=1); namespace Acme\SyliusExamplePlugin\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; final class SyliusGatewayConfigurationType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('api_key', TextType::class); } }And add its configuration to src/Resources/config/services.xml or
src/Resources/config/services.yaml
:<!-- src/Resources/config/services.xml --> <service id="Acme\SyliusExamplePlugin\Form\Type\SyliusGatewayConfigurationType"> <tag name="sylius.gateway_configuration_type" type="sylius_payment" label="Sylius Payment" /> <tag name="form.type" /> </service># src/Resources/config/services.yaml Acme\SyliusExamplePlugin\Form\Type\SyliusGatewayConfigurationType: tags: - { name: sylius.gateway_configuration_type, type: sylius_payment, label: 'Sylius Payment' } - { name: form.type }
4. To introduce support for new configuration fields, we need to create a value object which will be passed to action, so we can use an API Key provided in form.
Create a new ValueObject in
src/Payum/SyliusApi.php
:// src/Payum/SyliusApi.php <?php declare(strict_types=1); namespace Acme\SyliusExamplePlugin\Payum; final class SyliusApi { /** @var string */ private $apiKey; public function __construct(string $apiKey) { $this->apiKey = $apiKey; } public function getApiKey(): string { return $this->apiKey; } }In
src/Payum/SyliusPaymentGatewayFactory.php
we need to add support for newly createdSyliusApi
VO by adding$config['payum.api'] = function (ArrayObject $config) { return new SyliusApi($config['api_key']); };
at the end ofpopulateConfig
method. AdjustedSyliusPaymentGatewayFactory
class should look like this:// src/Payum/SyliusPaymentGatewayFactory.php <?php declare(strict_types=1); namespace Acme\SyliusExamplePlugin\Payum; use Payum\Core\Bridge\Spl\ArrayObject; use Payum\Core\GatewayFactory; final class SyliusPaymentGatewayFactory extends GatewayFactory { protected function populateConfig(ArrayObject $config): void { $config->defaults([ 'payum.factory_name' => 'sylius_payment', 'payum.factory_title' => 'Sylius Payment', ]); $config['payum.api'] = function (ArrayObject $config) { return new SyliusApi($config['api_key']); }; } }From now on, your new Payment Gateway should be available in the admin panel.
5. Configure new payment method in the admin panel
6. Configure required actions
We will create two actions: CaptureAction and StatusAction. The first one will be responsible for sending data to an external system:
payment amount
currency
API key configured in the previously created form
while the second one will translate HTTP codes of the Response to a proper state of payment.
6.1. Create StatusAction
and add it to the SyliusPaymentGatewayFactory
In a gateway factory class in
src/Payum/SyliusPaymentGatewayFactory.php
we need to add'payum.action.status' => new StatusAction(),
to config defaults. AdjustedSyliusPaymentGatewayFactory
class should look like this:// src/Payum/SyliusPaymentGatewayFactory.php <?php declare(strict_types=1); namespace Acme\SyliusExamplePlugin\Payum; use Acme\SyliusExamplePlugin\Payum\Action\StatusAction; use Payum\Core\Bridge\Spl\ArrayObject; use Payum\Core\GatewayFactory; final class SyliusPaymentGatewayFactory extends GatewayFactory { protected function populateConfig(ArrayObject $config): void { $config->defaults([ 'payum.factory_name' => 'sylius_payment', 'payum.factory_title' => 'Sylius Payment', 'payum.action.status' => new StatusAction(), ]); $config['payum.api'] = function (ArrayObject $config) { return new SyliusApi($config['api_key']); }; } }Now we need to create a
StatusAction
insrc/Payum/Action/StatusAction.php
:// src/Payum/Action/StatusAction.php <?php declare(strict_types=1); namespace Acme\SyliusExamplePlugin\Payum\Action; use Payum\Core\Action\ActionInterface; use Payum\Core\Exception\RequestNotSupportedException; use Payum\Core\Request\GetStatusInterface; use Sylius\Component\Core\Model\PaymentInterface as SyliusPaymentInterface; final class StatusAction implements ActionInterface { public function execute($request): void { RequestNotSupportedException::assertSupports($this, $request); /** @var SyliusPaymentInterface $payment */ $payment = $request->getFirstModel(); $details = $payment->getDetails(); if (200 === $details['status']) { $request->markCaptured(); return; } if (400 === $details['status']) { $request->markFailed(); return; } } public function supports($request): bool { return $request instanceof GetStatusInterface && $request->getFirstModel() instanceof SyliusPaymentInterface ; } }
StatusAction
will update the state of payment based on details provided byCaptureAction
. Based on the value of the status code of the HTTP request, the payment status will be adjusted as follows:
HTTP 400 (Bad request) - payment has failed
HTTP 200 (OK) - payment succeeded
6.2. Create a service for handling the CaptureAction
Warning
An external request interceptor was used for training purposes. Please, visit Beeceptor. and supply
sylius-payment
as an endpoint name. If the service is not working, you can use Post Test Server V2. as well, but remember about adjusting thehttps://sylius-payment.free.beeceptor.com
path.This time we will start with creating a
CaptureAction
insrc/Payum/Action/CaptureAction.php
:// src/Payum/Action/CaptureAction.php <?php declare(strict_types=1); namespace Acme\SyliusExamplePlugin\Payum\Action; use Acme\SyliusExamplePlugin\Payum\SyliusApi; use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; use Payum\Core\Action\ActionInterface; use Payum\Core\ApiAwareInterface; use Payum\Core\Exception\RequestNotSupportedException; use Payum\Core\Exception\UnsupportedApiException; use Sylius\Component\Core\Model\PaymentInterface as SyliusPaymentInterface; use Payum\Core\Request\Capture; final class CaptureAction implements ActionInterface, ApiAwareInterface { /** @var Client */ private $client; /** @var SyliusApi */ private $api; public function __construct(Client $client) { $this->client = $client; } public function execute($request): void { RequestNotSupportedException::assertSupports($this, $request); /** @var SyliusPaymentInterface $payment */ $payment = $request->getModel(); try { $response = $this->client->request('POST', 'https://sylius-payment.free.beeceptor.com', [ 'body' => json_encode([ 'price' => $payment->getAmount(), 'currency' => $payment->getCurrencyCode(), 'api_key' => $this->api->getApiKey(), ]), ]); } catch (RequestException $exception) { $response = $exception->getResponse(); } finally { $payment->setDetails(['status' => $response->getStatusCode()]); } } public function supports($request): bool { return $request instanceof Capture && $request->getModel() instanceof SyliusPaymentInterface ; } public function setApi($api): void { if (!$api instanceof SyliusApi) { throw new UnsupportedApiException('Not supported. Expected an instance of ' . SyliusApi::class); } $this->api = $api; } }And at the end of
src/Resources/config/services.xml
or src/Resources/config/services.yaml` add such a configuration for your capture action:<!-- src/Resources/config/services.xml --> <service id="Acme\SyliusExamplePlugin\Payum\Action\CaptureAction" public=true> <argument type="service" id="sylius.http_client" /> <tag name="payum.action" factory="sylius_payment" alias="payum.action.capture" /> </service># src/Resources/config/services.yaml Acme\SyliusExamplePlugin\Payum\Action\CaptureAction: public: true arguments: - '@sylius.http_client' tags: - { name: payum.action, factory: sylius_payment, alias: payum.action.capture }Your shop is ready to handle the first checkout with your newly created gateway!
Tip
On both previously mentioned interceptors, you may configure a status code of the response. Check the behavior of Sylius for 400 status code (HTTP Bad Request) as well!