Payment Plugins
A payment plugin is a add-ons to extend payment options
Payment is an essential part of the checkout process. A payment plugin is a add-ons that is used to process payments for eCommerce websites. It allows the customer to make a purchase without leaving the website and it also provides an interface for the store owner to manage their orders and customers.
First , you have to visit to plugin basics to get detailed information :
This content explains only how to develop payment plugin gateway.
Payment plugins have 3 type :
If you want to develop card based payment plugin , then you have to implement in your plugin gateway
CardPaymentGatewayInterface
Example Stripe gateway:
<?php
namespace Uvodo\Stripe;
use Brick\Money\Exception\UnknownCurrencyException;
use Framework\Http\StatusCodes;
use InvalidArgumentException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriFactoryInterface;
use RuntimeException;
use Support\PaymentGateway\CardPaymentGatewayInterface;
use Support\PaymentGateway\Exceptions\InvalidRequestException;
use Support\PaymentGateway\Exceptions\CardDeclinedException;
use Support\PaymentGateway\PaymentRequest;
use Support\PaymentGateway\PaymentResponseInterface;
use Support\PaymentGateway\SuccessResponse;
use Support\PaymentGateway\ValueObjects\Card;
/** @package Plugins\Stripe */
class StripePaymentGateway implements CardPaymentGatewayInterface
{
public const SHORT_NAME = "stripe";
private ClientInterface $httpClient;
private RequestFactoryInterface $httpRequestFactory;
private UriFactoryInterface $uriFactory;
private StreamFactoryInterface $streamFactory;
private string $host = 'api.stripe.com';
private string $scheme = "https";
private string $version = 'v1';
private SecretKey $privateKey;
/**
* @param ClientInterface $httpClient
* @param RequestFactoryInterface $httpRequestFactory
* @param UriFactoryInterface $uriFactory
* @param StreamFactoryInterface $streamFactory
* @param SecretKey $privateKey
* @return void
*/
public function __construct(
ClientInterface $httpClient,
RequestFactoryInterface $httpRequestFactory,
UriFactoryInterface $uriFactory,
StreamFactoryInterface $streamFactory,
SecretKey $privateKey
) {
$this->httpClient = $httpClient;
$this->httpRequestFactory = $httpRequestFactory;
$this->uriFactory = $uriFactory;
$this->streamFactory = $streamFactory;
$this->privateKey = $privateKey;
}
/**
* @inheritDoc
*/
public function getShortName(): string
{
return self::SHORT_NAME;
}
/**
* @inheritDoc
* @throws ClientExceptionInterface
*/
public function pay(PaymentRequest $request): PaymentResponseInterface
{
/**
* @todo body details here
*/
$body = [
'amount' => $request->amount->getValue(),
'currency' => $request->currencyCode->getValue(),
];
if ($request->card) {
$token = $this->createCardToken($request->card);
$body['source'] = $token;
}
$resp = $this->sendRequest('POST', '/charges', $body);
$json = json_decode($resp->getBody()->getContents());
$authorization = $json->id;
$payResp = new SuccessResponse();
$payResp->setAuthorization($authorization);
return $payResp;
}
/**
* @param string $method
* @param string $path
* @param null|array $body
* @return ResponseInterface
* @throws InvalidArgumentException
* @throws ClientExceptionInterface
* @throws RuntimeException
* @throws CardDeclinedException
* @throws InvalidRequestException
*/
private function sendRequest(
string $method,
string $path,
?array $body = null
): ResponseInterface {
$req = $this->createHttpRequest($method, $path);
if ($body) {
$stream = $this->createStreamBody($body);
$req = $req->withBody($stream);
}
$resp = $this->httpClient->sendRequest($req);
$this->validateResponse($resp);
return $resp;
}
/**
* @param string $method
* @param string $path
* @return RequestInterface
* @throws InvalidArgumentException
*/
private function createHttpRequest(
string $method,
string $path
): RequestInterface {
$uri = $this->uriFactory->createUri('/' . $this->version . $path)
->withHost($this->host)
->withScheme($this->scheme);
$req = $this->httpRequestFactory->createRequest($method, $uri)
->withHeader(
'Authorization',
'Bearer ' . $this->privateKey->getValue()
);
return $req;
}
/**
* @param array $body
* @return StreamInterface
*/
private function createStreamBody(array $body): StreamInterface
{
return $this->streamFactory->createStream(http_build_query($body));
}
/**
* @param Card $card
* @return string
* @throws InvalidArgumentException
* @throws ClientExceptionInterface
* @throws RuntimeException
* @throws CardDeclinedException
* @throws InvalidRequestException
*/
private function createCardToken(Card $card): string
{
$body = [
'card' => [
'number' => $card->number->getValue(),
'exp_month' => $card->expMonth->getValue(),
'exp_year' => $card->expYear->getValue(),
'cvc' => $card->cvc->getValue()
],
];
$resp = $this->sendRequest('POST', '/tokens', $body);
$json = json_decode($resp->getBody()->getContents());
return $json->id;
}
/**
* @param ResponseInterface $resp
* @return void
* @throws RuntimeException
* @throws CardDeclinedException
* @throws InvalidRequestException
*/
private function validateResponse(ResponseInterface $resp): void
{
if (
$resp->getStatusCode() < StatusCodes::HTTP_OK
|| $resp->getStatusCode() >= StatusCodes::HTTP_MULTIPLE_CHOICES
) {
$json = json_decode($resp->getBody()->getContents());
$error = $json->error;
$code = $error->code;
if ($code == 'card_declined') {
throw new CardDeclinedException(
$error->decline_code,
$error->message
);
}
throw new InvalidRequestException($error->message, $code, $error->param ?? null);
}
}
}
If you want to develop external payment plugin , then you have to implement in your plugin gateway
OtherPaymentGatewayInterface
Example Mercado Pago gateway:
<?php
namespace Uvodo\MercadoPago;
use Brick\Money\Exception\UnknownCurrencyException;
use Brick\Money\Money;
use Framework\Http\StatusCodes;
use Modules\Order\Domain\Entities\OrderItemEntity;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
use Support\PaymentGateway\Exceptions\InvalidRequestException;
use Support\PaymentGateway\OtherPaymentGatewayInterface;
use Support\PaymentGateway\PaymentConfirmationRequest;
use Support\PaymentGateway\PaymentRequest;
use Support\PaymentGateway\PaymentResponseInterface;
use Support\PaymentGateway\RedirectResponse;
use Support\PaymentGateway\SuccessResponse;
use Uvodo\MercadoPago\Exceptions\PaymentException;
/** @package Plugins\MercadoPago */
class MercadoPagoPaymentGateway implements OtherPaymentGatewayInterface
{
public const SHORT_NAME = "mercado-pago";
private string $host;
private string $scheme = "https";
private string $version = 'v1';
private const STATUS_APPROVED = 'approved';
private const STATUS_DECLINED = 'declined';
/**
* @param ClientInterface $httpClient
* @param RequestFactoryInterface $httpRequestFactory
* @param UriFactoryInterface $uriFactory
* @param StreamFactoryInterface $streamFactory
* @param ApiTokenKey $apiTokenKey
*/
public function __construct(
private ClientInterface $httpClient,
private RequestFactoryInterface $httpRequestFactory,
private UriFactoryInterface $uriFactory,
private StreamFactoryInterface $streamFactory,
private ApiTokenKey $apiTokenKey
) {
$this->host = 'api.mercadopago.com';
}
public function getShortName(): string
{
return self::SHORT_NAME;
}
/**
* @param PaymentRequest $request
* @return PaymentResponseInterface
* @throws ClientExceptionInterface
* @throws InvalidRequestException|UnknownCurrencyException
*/
public function pay(PaymentRequest $request): PaymentResponseInterface
{
$data = $this->createPreference($request);
$payResp = new RedirectResponse();
$payResp->setAuthorization($data->id);
$payResp->setRedirectUrl($data->init_point);
return $payResp;
}
/**
*/
public function confirm(PaymentConfirmationRequest $request): PaymentResponseInterface
{
parse_str($request->queryString, $params);
$payment = $this->getPayment($params['payment_id']);
if (is_null($payment)) {
throw new PaymentException("Payment is invalid");
}
if ($payment->status === self::STATUS_APPROVED) {
$res = new SuccessResponse();
$res->setAuthorization($params['payment_id']);
return $res;
} elseif ($payment->status === self::STATUS_DECLINED) {
throw new PaymentException("Payment was declined");
}
throw new PaymentException("Payment is invalid");
}
/**
* @param PaymentRequest $request
* @return mixed
* @throws ClientExceptionInterface
* @throws InvalidRequestException|UnknownCurrencyException
*/
private function createPreference(
PaymentRequest $request
): mixed {
$uri = $this->uriFactory->createUri('/checkout/preferences')
->withHost($this->host)
->withScheme($this->scheme);
$redirectUrl = env('STOREFRONT_URL') . '/checkout/' . $request->cardToken->getValue() . '?';
$returnQuery = http_build_query(
[
'gateway' => self::SHORT_NAME,
'redirect_type' => 'return'
]
);
$cancelQuery = http_build_query(
[
'gateway' => self::SHORT_NAME,
'redirect_type' => 'cancel'
]
);
$backUrls = [
'success' => $redirectUrl . $returnQuery,
'pending' => $redirectUrl . $cancelQuery,
'failure' => $redirectUrl . $cancelQuery,
];
$currency = $request->order->getShop()->getCurrencyCode()->getValue();
$items = [];
$payer = [
'name' => $request->order->getCustomer()->getFirstName()->getValue(),
'surname' => $request->order->getCustomer()->getLastName()->getValue(),
'email' => $request->order->getCustomer()->getEmail()->getValue(),
'phone' => $request->order->getCustomer()->getPhoneNumber()->getValue(),
];
/** @var OrderItemEntity $item */
foreach ($request->order->getItems() as $item) {
$price = $item->getSalePrice() && $item->getSalePrice()->getValue() ?: $item->getPrice();
if (!is_null($price) && $price->getValue() > 0) {
$price = Money::ofMinor($price->getValue(), $currency);
$major = $price->getAmount()->getIntegralPart();
$minor = $price->getAmount()->getFractionalPart();
$items[] = [
'id' => $item->getId()->getValue(),
'title' => $item->getProduct()->getTitle()->getValue(),
'description' => $item->getProduct()->getMetaDescription()->getValue(),
'quantity' => $item->getQuantity()->getValue(),
'currency_id' => $currency,
'unit_price' => (float)($major . '.' . $minor)
];
}
}
$shippingCost = $request->order->getShippingCost()->getValue();
if (!is_null($shippingCost)) {
$shippingCost = Money::ofMinor($shippingCost, $currency);
$major = $shippingCost->getAmount()->getIntegralPart();
$minor = $shippingCost->getAmount()->getFractionalPart();
$items[] = [
'id' => rand(1000, 9000),
'title' => 'Shipping Cost',
'description' => 'The amount is shipping cost for order',
'quantity' => 1,
'currency_id' => $currency,
'unit_price' => (float)($major . '.' . $minor)
];
}
$taxCost = $request->order->getTotalTax()->getValue();
if (!is_null($taxCost)) {
$taxCost = Money::ofMinor($taxCost, $currency);
$major = $taxCost->getAmount()->getIntegralPart();
$minor = $taxCost->getAmount()->getFractionalPart();
$items[] = [
'id' => rand(1000, 9000),
'title' => 'Tax Cost',
'description' => 'The amount is tax cost for order',
'quantity' => 1,
'currency_id' => $currency,
'unit_price' => (float)($major . '.' . $minor)
];
}
$body = $this->streamFactory->createStream(json_encode([
"items" => $items,
"back_urls" => $backUrls,
'auto_return' => 'all',
'payer' => $payer
]));
$req = $this->httpRequestFactory->createRequest('POST', $uri)
->withHeader('Content-Type', 'application/json')
->withHeader(
'Authorization',
'Bearer ' . $this->apiTokenKey->getValue()
)->withBody($body);
$resp = $this->httpClient->sendRequest($req);
$this->validateResponse($resp);
return json_decode($resp->getBody()->getContents());
}
private function getPayment(string $paymentId)
{
$uri = $this->uriFactory->createUri($this->version . '/payments/' . $paymentId)
->withHost($this->host)
->withScheme($this->scheme);
$req = $this->httpRequestFactory->createRequest('GET', $uri)
->withHeader('Content-Type', 'application/json')
->withHeader(
'Authorization',
'Bearer ' . $this->apiTokenKey->getValue()
);
$resp = $this->httpClient->sendRequest($req);
$this->validateResponse($resp);
return json_decode($resp->getBody()->getContents());
}
/**
* @param ResponseInterface $res
* @return void
* @throws InvalidRequestException
*/
private function validateResponse(ResponseInterface $res): void
{
if ($res->getStatusCode() > StatusCodes::HTTP_CREATED) {
$json = json_decode($res->getBody()->getContents());
$description = $json->Message ?? '';
throw new InvalidRequestException($description, $res->getStatusCode());
}
}
}
If you want to develop cash payment plugin , then you have to implement in your plugin gateway
ManualPaymentGatewayInterface
Example COD gateway:
<?php
namespace Uvodo\CashOnDelivery;
use Support\PaymentGateway\ManualPaymentGatewayInterface;
use Support\PaymentGateway\PaymentRequest;
use Support\PaymentGateway\PaymentResponseInterface;
use Support\PaymentGateway\SuccessResponse;
/** @package Plugins\CashOnDelivery */
class CashOnDeliveryPaymentGateway implements ManualPaymentGatewayInterface
{
public const SHORT_NAME = "cod";
/** @inheritDoc */
public function getShortName(): string
{
return self::SHORT_NAME;
}
/** @inheritDoc */
public function pay(PaymentRequest $request): PaymentResponseInterface
{
$payResp = new SuccessResponse();
$payResp->setAuthorization(uniqid('cod_'));
return $payResp;
}
}
After completing your payment plugin , you have to register your gateway in payment gateway service. To use payment factory just add
PaymentGatewayFactory
to constructor in your plugin's entry class. Example here:
public function __construct(
ContainerInterface $container,
PaymentGatewayFactory $gatewayFactory,
RoutingBootstrapper $rb,
OptionHelper $optionHelper
) {
}
In your plugin's entry class , register the gateway inside
boot
function:$this->paymentFactory
->registerPaymentGateway(
CashOnDeliveryPaymentGateway::SHORT_NAME,
CashOnDeliveryPaymentGateway::class,
$context
);
First parameter is short name for plugin , second parameter is your gateway class which you have already developed. Short name has to be unique to determine from where request coming.
That's it. Your payment plugin is ready to build. Just run below command to get builded zip:
php bin/console plugin:build
You will see below screen:

Choose a plugin number which one you want to build:
After completing the command , build zip will be generated inside
build
folder in builded plugin folder.
You can upload the zip with 2 ways.
- 1.via Admin Plugin Upload
- 2.via Command Line

Or you can use install command:
php bin/console plugin:install

Type there your plugin's builded zip path
And that's it , plugin already ready to use
🎉
🎉
Last modified 3mo ago