mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-14 17:10:53 +00:00
[Docs] How to integrate a Payment Gateway as a Plugin?
This commit is contained in:
parent
71dca46dea
commit
998b7e9747
5 changed files with 375 additions and 0 deletions
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 235 KiB |
|
|
@ -51,6 +51,7 @@ Payments
|
|||
payments/stripe
|
||||
payments/encrypting-gateway-config
|
||||
payments/authorize
|
||||
payments/custom-payment-gateway
|
||||
|
||||
.. include:: /cookbook/payments/map.rst.inc
|
||||
|
||||
|
|
|
|||
373
docs/cookbook/payments/custom-payment-gateway.rst
Normal file
373
docs/cookbook/payments/custom-payment-gateway.rst
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
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 <https://github.com/Sylius/PluginSkeleton>`_.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ 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``:
|
||||
|
||||
.. code-block:: 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`` add such a configuration for your gateway:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
# 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>
|
||||
|
||||
**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``:
|
||||
|
||||
.. code-block:: 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`:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
# 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>
|
||||
|
||||
**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``:
|
||||
|
||||
.. code-block:: 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 created ``SyliusApi`` VO by adding
|
||||
``$config['payum.api'] = function (ArrayObject $config) { return new SyliusApi($config['api_key']); };`` at the end of
|
||||
``populateConfig`` method. Adjusted ``SyliusPaymentGatewayFactory`` class should look like this:
|
||||
|
||||
.. code-block:: 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',
|
||||
]);
|
||||
|
||||
$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.
|
||||
|
||||
.. image:: ../../_images/cookbook/custom-payment-gateway/new_gateway_configuration_type.png
|
||||
|
||||
**5.** Configure new payment method in the admin panel
|
||||
|
||||
.. image:: ../../_images/cookbook/custom-payment-gateway/new_payment_method.png
|
||||
|
||||
**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. Adjusted ``SyliusPaymentGatewayFactory`` class
|
||||
should look like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// 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`` in ``src/Payum/Action/StatusAction.php``:
|
||||
|
||||
.. code-block:: 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 by ``CaptureAction``.
|
||||
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 <https://beeceptor.com/>`_. and supply ``sylius-payment`` as an endpoint name. If the service
|
||||
is not working, you can use `Post Test Server V2 <https://ptsv2.com/>`_. as well, but remember about adjusting
|
||||
the ``https://sylius-payment.free.beeceptor.com`` path.
|
||||
|
||||
This time we will start with creating a ``CaptureAction`` in ``src/Payum/Action/CaptureAction.php``:
|
||||
|
||||
.. code-block:: 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`` add such a configuration for your capture action:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
# 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>
|
||||
|
||||
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!
|
||||
|
||||
Learn more
|
||||
----------
|
||||
|
||||
* :doc:`Order payments documentation </book/orders/payments>`
|
||||
* `Payum documentation <https://github.com/Payum/Payum/blob/master/docs/index.md>`_.
|
||||
* `Mollie payment integration <https://github.com/BitBagCommerce/SyliusMolliePlugin/>`_.
|
||||
|
|
@ -2,3 +2,4 @@
|
|||
* :doc:`/cookbook/payments/stripe`
|
||||
* :doc:`/cookbook/payments/encrypting-gateway-config`
|
||||
* :doc:`/cookbook/payments/authorize`
|
||||
* :doc:`/cookbook/payments/custom-payment-gateway`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue