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:
<?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:
<?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):
<?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:
<?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:
<?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?