Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Eigene Zahlungsarten in Magento 2: Grundlagen und die Adapter-Facade

Eigene Zahlungsarten in Magento 2: Grundlagen und die Adapter-Facade

~7 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026

Block 7 hat gezeigt, dass Magento manchmal eine vom Core vorgegebene Basisklasse erzwingt, die trotzdem keine Ausrede für duplizierte Geschäftslogik ist - ContentTypeAbstract bei Page Builder (Kapitel 59), Template bei Widgets (Kapitel 56). Block 8 trifft auf denselben Musterfall gleich zweimal: bei Zahlungsarten und bei Versandarten. Dieses Kapitel beginnt mit Zahlungsarten und legt dafür erst das technische Fundament, bevor Kapitel 63 die konkrete "Punkte einlösen"-Zahlungsart baut.

Der alte Weg: AbstractMethod

Vor Magento 2.2 - und in vielen älteren Drittanbieter-Erweiterungen bis heute - erbte eine Zahlungsart direkt von Magento\Payment\Model\Method\AbstractMethod. Methodencode, Verfügbarkeits-Flags und Gateway-Logik lebten alle in derselben Klasse, oft als öffentliche Properties:

(nur zur Einordnung, keine Datei dieses Moduls)
<?php

declare(strict_types=1);

namespace Vendor\Module\Model\Payment;

use Magento\Payment\Model\Method\AbstractMethod;
use Magento\Quote\Api\Data\CartInterface;

/**
 * Legacy-style payment method (pre-2.2 pattern), shown only for recognition -
 * NOT the pattern this chapter builds on for Mironsoft\Loyalty.
 */
class LegacyExampleMethod extends AbstractMethod
{
    /**
     * Method code, referenced by payment/vendor_module_legacy_example/* config paths.
     */
    protected $_code = 'vendor_module_legacy_example';

    /**
     * Offline method, no gateway communication.
     */
    protected $_isOffline = true;

    /**
     * Storefront checkout availability, historically a plain public property.
     */
    protected $_canUseCheckout = true;

    /**
     * Admin order creation availability.
     */
    protected $_canUseInternal = false;

    /**
     * Legacy availability hook - tightly bound to this very instance, hard to
     * unit test without instantiating the whole method object first.
     *
     * @param CartInterface|null $quote Current quote
     * @return bool
     */
    public function isAvailable(CartInterface $quote = null): bool
    {
        return parent::isAvailable($quote) && $quote !== null;
    }
}

Achtung: AbstractMethod funktioniert nach wie vor - Magento hat die Klasse nie entfernt. Der Grund, warum dieses Kapitel trotzdem den moderneren Weg wählt: Eine Instanzmethode wie isAvailable() lässt sich nur zusammen mit der gesamten Zahlungsart-Instanz testen, Verhalten und Konfiguration sind untrennbar verwoben, und jede kleine Anpassung (z. B. ein neuer Verfügbarkeits-Check) bedeutet eine neue Unterklasse statt eines neu zusammengesteckten Bausteins.

Der moderne Weg: die Adapter-Facade

Seit Magento 2.2 gibt es Magento\Payment\Model\Method\Adapter - eine generische, fertige Implementierung von MethodInterface, die selbst keine Geschäftslogik enthält, sondern sie an austauschbare Mitarbeiter delegiert: einen ValueHandlerPool für Konfigurationswerte und Verfügbarkeit, optional einen ValidatorPool und einen CommandPool für tatsächliche Gateway-Aufrufe (Autorisierung, Capture - für die rein rabattbasierte "Punkte einlösen"-Methode aus Kapitel 63 nicht nötig, da kein echtes Gateway dahintersteckt). Eine konkrete Zahlungsart entsteht dadurch fast ausschließlich per di.xml-virtualType - Komposition statt Vererbung, derselbe Grundsatz, der in diesem Projekt auch Plugins gegenüber Preferences bevorzugt (siehe CLAUDE.md).

Ein einziges echtes PHP-Artefakt bleibt trotzdem nötig: eine Klasse, die den Methodencode als Konstante hält und - da die Zahlungsart auf Kundendaten reagieren soll - die Checkout-Konfiguration für das Frontend bereitstellt:

app/code/Mironsoft/Loyalty/Model/Payment/PointsRedemptionConfigProvider.php
<?php

declare(strict_types=1);

namespace Mironsoft\Loyalty\Model\Payment;

use Magento\Checkout\Model\ConfigProviderInterface;
use Magento\Customer\Model\Session as CustomerSession;
use Mironsoft\Loyalty\Model\Config\LoyaltyConfig;

/**
 * Exposes the loyalty payment method's code and the current customer's points
 * balance to the checkout JavaScript layer via window.checkoutConfig.
 */
class PointsRedemptionConfigProvider implements ConfigProviderInterface
{
    /**
     * Payment method code. Single source of truth, referenced from di.xml (as a
     * const argument) and read back out of window.checkoutConfig by the checkout
     * JS component in chapter 65 - both sides always agree on the exact string.
     */
    public const string METHOD_CODE = 'mironsoft_loyalty_points';

    /**
     * @param CustomerSession $customerSession Frontend customer session
     * @param LoyaltyConfig $loyaltyConfig Loyalty module configuration reader
     */
    public function __construct(
        private readonly CustomerSession $customerSession,
        private readonly LoyaltyConfig $loyaltyConfig,
    ) {
    }

    /**
     * Builds the configuration array merged into window.checkoutConfig.
     *
     * @return array<string, mixed>
     */
    public function getConfig(): array
    {
        $balance = $this->customerSession->isLoggedIn()
            ? (int) $this->customerSession->getCustomer()->getData('loyalty_points_balance')
            : 0;

        return [
            'mironsoftLoyalty' => [
                'methodCode' => self::METHOD_CODE,
                'pointsBalance' => $balance,
                'pointsPerEuro' => $this->loyaltyConfig->getPointsPerEuro(),
            ],
        ];
    }
}

payment.xml und die Registrierung über config.xml

etc/payment.xml ist die deklarative Schnittstelle für methodenweite Eigenschaften, die keinen Konfigurationswert im klassischen Sinn darstellen - hier nur allow_multiple_address, das steuert, ob die Methode bei einer Mehrfachadress-Bestellung überhaupt angeboten wird (bei punktebasierter Zahlung bewusst 0, siehe Kapitel 70 für die Begründung):

app/code/Mironsoft/Loyalty/etc/payment.xml
<?xml version="1.0"?>
<payment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/payment.xsd">
    <methods>
        <method name="mironsoft_loyalty_points">
            <allow_multiple_address>0</allow_multiple_address>
        </method>
    </methods>
</payment>

Welche Klasse Magento für den Methodencode mironsoft_loyalty_points tatsächlich instanziiert, steht dagegen nicht in payment.xml, sondern - genau wie später bei Versandarten in Kapitel 66 - als model-Konfigurationswert in etc/config.xml:

app/code/Mironsoft/Loyalty/etc/config.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <payment>
            <mironsoft_loyalty_points>
                <active>0</active>
                <model>Mironsoft\Loyalty\Model\Payment\PointsRedemptionFacade</model>
                <title>Mit Treuepunkten bezahlt</title>
                <can_use_checkout>1</can_use_checkout>
                <can_use_internal>0</can_use_internal>
                <sort_order>5</sort_order>
            </mironsoft_loyalty_points>
        </payment>
    </default>
</config>

Und die Adapter-Facade selbst, zusammen mit dem ValueHandlerPool und der Registrierung des ConfigProviders:

app/code/Mironsoft/Loyalty/etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <virtualType name="Mironsoft\Loyalty\Model\Payment\ValueHandlerPool"
                 type="Magento\Payment\Gateway\Config\ValueHandlerPool">
        <arguments>
            <argument name="handlers" xsi:type="array">
                <item name="default" xsi:type="string">Magento\Payment\Gateway\Config\ConfigValueHandler</item>
                <!-- the "availability" entry is added in chapter 64 -->
            </argument>
        </arguments>
    </virtualType>

    <virtualType name="Mironsoft\Loyalty\Model\Payment\PointsRedemptionFacade"
                 type="Magento\Payment\Model\Method\Adapter">
        <arguments>
            <argument name="code" xsi:type="const">Mironsoft\Loyalty\Model\Payment\PointsRedemptionConfigProvider::METHOD_CODE</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">Mironsoft\Loyalty\Model\Payment\ValueHandlerPool</argument>
        </arguments>
    </virtualType>

    <type name="Magento\Checkout\Model\CompositeConfigProvider">
        <arguments>
            <argument name="configProviders" xsi:type="array">
                <item name="mironsoft_loyalty_points" xsi:type="object">Mironsoft\Loyalty\Model\Payment\PointsRedemptionConfigProvider</item>
            </argument>
        </arguments>
    </type>
</config>

Tipp: can_use_checkout und can_use_internal in config.xml sind bereits die moderne Antwort auf AbstractMethods $_canUseCheckout/$_canUseInternal von oben: Der Adapter delegiert jede can*-Fähigkeitsabfrage an den default-Handler des ValueHandlerPools, der einfach den passenden payment/<code>/<feld>-Konfigurationswert liest. Kapitel 64 vertieft das.

Damit ist die Zahlungsart technisch registriert, aber noch funktionslos - sie erscheint nicht im Checkout (deaktiviert) und bucht noch keine Punkte. Kapitel 63 füllt genau diese Lücke, und klärt zuerst die wichtigste Design-Frage: Wie passt eine "Teilzahlung mit Punkten" überhaupt in ein System, das pro Bestellung nur eine einzige Zahlungsart kennt?