Payment Methods

Magento payment methods are built by configuring the core adapter with gateway commands, not by writing MethodInterface.

Syntax<virtualType name="VendorPayFacade" type="Magento\Payment\Model\Method\Adapter">

A payment method in Magento 2.4.9 is any class implementing Magento\Payment\Model\MethodInterface. In practice you do not write that class. You configure a copy of Magento\Payment\Model\Method\Adapter as a virtual type, a named pre-configured instance of an existing class, and plug small single-purpose classes into it. That set of pieces is the gateway framework, and it lives entirely in Magento Open Source.

The pieces

A command is one gateway operation: authorize, capture, void, refund. Each is a Magento\Payment\Gateway\Command\GatewayCommand virtual type wired from four interfaces. Request\BuilderInterface::build(array $buildSubject) turns the payment into an array. Http\TransferFactoryInterface::create(array $request) wraps it. Http\ClientInterface::placeRequest() sends it. Response\HandlerInterface::handle(array $handlingSubject, array $response) writes the result back onto the payment. A Validator\ValidatorInterface can reject the response before the handler sees it. Admin settings reach the method through a Config\ValueHandlerPool reading payment/<code>/*.

The vault

The vault stores a reusable gateway token as a Magento\Vault\Api\Data\PaymentTokenInterface row: a public hash, the gateway token, an expiry and masked card details. A returning customer then pays without re-entering the card. Tokens hang off a customer id, so a guest checkout produces none.

What a real integration must handle

Authorize-only as well as authorize-and-capture; partial capture and partial refund; void before settlement; a redirect or 3-D Secure step that takes the browser away and returns; and an asynchronous webhook that arrives after the order already exists.

Virtual types are resolved from generated code in production mode, so run bin/magento setup:di:compile after editing di.xml, and bin/magento cache:flush config after config.xml.

Example

Example · xml
<!-- app/code/Vendor/Pay/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <!-- The payment method itself: the core adapter, configured. -->
    <virtualType name="VendorPayFacade" type="Magento\Payment\Model\Method\Adapter">
        <arguments>
            <argument name="code" xsi:type="string">vendor_pay</argument>
            <argument name="formBlockType" xsi:type="string">Magento\Payment\Block\Form</argument>
            <argument name="infoBlockType" xsi:type="string">Magento\Payment\Block\Info</argument>
            <argument name="valueHandlerPool" xsi:type="object">VendorPayValueHandlerPool</argument>
            <argument name="commandPool" xsi:type="object">VendorPayCommandPool</argument>
        </arguments>
    </virtualType>

    <!-- Reads payment/vendor_pay/* from configuration. -->
    <virtualType name="VendorPayConfig" type="Magento\Payment\Gateway\Config\Config">
        <arguments>
            <argument name="methodCode" xsi:type="string">vendor_pay</argument>
        </arguments>
    </virtualType>
    <virtualType name="VendorPayConfigValueHandler" type="Magento\Payment\Gateway\Config\ConfigValueHandler">
        <arguments>
            <argument name="configInterface" xsi:type="object">VendorPayConfig</argument>
        </arguments>
    </virtualType>
    <virtualType name="VendorPayValueHandlerPool" type="Magento\Payment\Gateway\Config\ValueHandlerPool">
        <arguments>
            <argument name="handlers" xsi:type="array">
                <item name="default" xsi:type="string">VendorPayConfigValueHandler</item>
            </argument>
        </arguments>
    </virtualType>

    <!-- One command per gateway operation. -->
    <virtualType name="VendorPayCommandPool" type="Magento\Payment\Gateway\Command\CommandPool">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="authorize" xsi:type="string">VendorPayAuthorizeCommand</item>
            </argument>
        </arguments>
    </virtualType>

    <virtualType name="VendorPayAuthorizeCommand" type="Magento\Payment\Gateway\Command\GatewayCommand">
        <arguments>
            <argument name="requestBuilder" xsi:type="object">Vendor\Pay\Gateway\Request\AuthorizeRequest</argument>
            <argument name="transferFactory" xsi:type="object">Vendor\Pay\Gateway\Http\TransferFactory</argument>
            <argument name="client" xsi:type="object">Vendor\Pay\Gateway\Http\Client</argument>
            <argument name="handler" xsi:type="object">Vendor\Pay\Gateway\Response\TxnIdHandler</argument>
        </arguments>
    </virtualType>
</config>

When to use it

  • Integrate a regional card gateway that has no Magento extension.
  • Add a capture command so the merchant can authorise now and charge on shipment.
  • Store a token so returning customers can reorder without re-entering a card.
  • Trace which class built the request a gateway rejected.

More examples

Pointing the method code at the virtual type

The model node holds the virtual type name, not a real class. That is the join between configuration and the di.xml wiring, and it is the usual reason a method never appears at checkout. Note how xsi:type differs by position in di.xml: a direct argument uses object, while a class or virtual type named inside an array uses string. Both resolve to the same instance. A separate etc/payment.xml declares the method's group and card types for the admin screens. Apply with bin/magento cache:flush config.

Example · xml
<!-- app/code/Vendor/Pay/etc/config.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <payment>
            <vendor_pay>
                <active>0</active>
                <model>VendorPayFacade</model>
                <title>Card</title>
                <payment_action>authorize</payment_action>
                <order_status>pending_payment</order_status>
                <currency>EUR</currency>
                <can_authorize>1</can_authorize>
                <can_capture>1</can_capture>
                <can_refund>1</can_refund>
                <can_void>1</can_void>
            </vendor_pay>
        </payment>
    </default>
</config>

Building the gateway request

Builders are the readable part of a gateway integration: one class, one array, no HTTP. SubjectReader is the supported way to pull the payment and amount out of the untyped $buildSubject array, and it throws InvalidArgumentException rather than returning null when a key is missing. Several builders can be composed with Magento\Payment\Gateway\Request\BuilderComposite when one request has distinct sections, such as customer, address and card.

Example · php
<?php
// app/code/Vendor/Pay/Gateway/Request/AuthorizeRequest.php
declare(strict_types=1);

namespace Vendor\Pay\Gateway\Request;

use Magento\Payment\Gateway\Helper\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;

class AuthorizeRequest implements BuilderInterface
{
    public function build(array $buildSubject): array
    {
        $paymentDO = SubjectReader::readPayment($buildSubject);
        $amount = SubjectReader::readAmount($buildSubject);
        $order = $paymentDO->getOrder();
        $payment = $paymentDO->getPayment();

        return [
            'txn_type' => 'authorize',
            'amount' => sprintf('%.2F', (float) $amount),
            'currency' => $order->getCurrencyCode(),
            'reference' => $order->getOrderIncrementId(),
            'nonce' => (string) $payment->getAdditionalInformation('vendor_pay_nonce'),
        ];
    }
}

Writing the gateway answer back onto the payment

Everything the merchant later needs, in the admin or in a refund, comes from what this handler stores. Leaving the transaction open is what makes Invoice and Credit Memo work against the original authorisation; closing it means every later operation has to start a fresh gateway transaction. Chain several handlers with Magento\Payment\Gateway\Response\HandlerChain when one response updates the payment, the fraud status and a vault token.

Example · php
<?php
// app/code/Vendor/Pay/Gateway/Response/TxnIdHandler.php
declare(strict_types=1);

namespace Vendor\Pay\Gateway\Response;

use Magento\Payment\Gateway\Helper\SubjectReader;
use Magento\Payment\Gateway\Response\HandlerInterface;
use Magento\Sales\Model\Order\Payment;

class TxnIdHandler implements HandlerInterface
{
    public function handle(array $handlingSubject, array $response): void
    {
        $paymentDO = SubjectReader::readPayment($handlingSubject);

        /** @var Payment $payment */
        $payment = $paymentDO->getPayment();
        $payment->setTransactionId((string) ($response['txn_id'] ?? ''));
        $payment->setCcTransId((string) ($response['txn_id'] ?? ''));

        // An authorisation is not final, so the transaction stays open for a later capture.
        $payment->setIsTransactionClosed(false);
        $payment->setShouldCloseParentTransaction(false);
    }
}

Discussion

  • Be the first to comment on this lesson.