im Magento-2-Checkout sauber aufbauen
Eine eigene Zahlungsart-Integration ist in Magento 2 kein einfaches Formular mit Kontodaten, sondern eine vollständige Gateway-Anbindung mit Autorisierung, Capture, Stornierung und asynchronen Statusupdates. Das Payment Gateway Command Pattern strukturiert diese Bausteine so, dass ein externer Zahlungsdienstleister ohne Core-Overrides angebunden werden kann.
Inhaltsverzeichnis
- 1. Warum eine eigene Zahlungsart-Integration mehr als ein Formular ist
- 2. Das Payment Gateway Command Pattern im Überblick
- 3. ConfigProvider: Konfiguration ins Frontend reichen
- 4. Commands registrieren: Authorize, Capture, Cancel
- 5. Value Handler und Response Validator für Gateway-Antworten
- 6. Die Zahlungsart im Hyvä-Checkout-Frontend rendern
- 7. Asynchrone Statusupdates per Webhook verarbeiten
- 8. Fehlerbehandlung und Idempotenz bei Zahlungsanfragen
- 9. Eigene Integration im Vergleich zur Payment-Bridge
- 10. Zusammenfassung
- 11. FAQ
1. Warum eine eigene Zahlungsart-Integration mehr als ein Formular ist
Wer eine Zahlungsart-Integration zum ersten Mal umsetzt, unterschätzt häufig, wie viele Zustandsübergänge ein Zahlungsvorgang tatsächlich durchläuft. Eine Bestellung wird autorisiert, teilweise oder vollständig eingezogen, möglicherweise storniert, im Nachhinein teilweise erstattet, und der externe Zahlungsdienstleister meldet Statusänderungen oft asynchron per Webhook, unabhängig vom ursprünglichen Checkout-Request. Eine Zahlungsart-Integration, die nur den Autorisierungsfall abdeckt, bricht spätestens bei der ersten Rückerstattung oder dem ersten verzögerten Zahlungseingang zusammen.
Magento adressiert diese Komplexität mit dem Payment Gateway Command Pattern, das seit Magento 2.1 die alte AbstractMethod-Klasse als empfohlenen Weg für eine neue Zahlungsart-Integration abgelöst hat. Statt einer monolithischen Zahlungsart-Klasse mit vielen Methoden entsteht eine Sammlung kleiner, einzeln testbarer Commands, die jeweils genau einen Zustandsübergang abbilden. Das deckt sich exakt mit den Projektvorgaben, Service Contracts und klar abgegrenzte Verantwortlichkeiten gegenüber großen God-Klassen zu bevorzugen.
Dieser Artikel begleitet den vollständigen Aufbau einer Zahlungsart-Integration für einen fiktiven Zahlungsdienstleister: ConfigProvider für die Frontend-Konfiguration, Command-Registrierung für Authorize, Capture und Cancel, Value Handler und Response Validator für die Gateway-Antworten, die Anbindung an das Hyvä-Checkout-Frontend sowie Webhook-Verarbeitung und Idempotenz. Alles orientiert sich an Magento 2.4.8-p4 mit PHP 8.4 und Constructor Property Promotion.
2. Das Payment Gateway Command Pattern im Überblick
Im Zentrum jeder modernen Zahlungsart-Integration steht der CommandPool, eine Registry, die einen Zustandsübergang, etwa authorize oder capture, auf eine konkrete Command-Klasse abbildet. Jeder Command implementiert CommandInterface mit genau einer execute()-Methode und erhält als Argument ein PaymentDataObject, das Order, Payment und Amount kapselt. Diese strikte Eins-zu-eins-Zuordnung zwischen Zustandsübergang und Command-Klasse ist der Kern des Command Patterns und macht jede einzelne Operation unabhängig testbar.
Ergänzend zum CommandPool gibt es den ConfigProvider, der Konfigurationswerte wie den API-Endpunkt oder den Public Key ans Checkout-Frontend reicht, sowie ValueHandlerPool und ResponseValidatorPool, die Gateway-Antworten in Magento-verständliche Werte beziehungsweise Erfolgs- oder Fehlerergebnisse übersetzen. Diese vier Bausteine zusammen bilden eine vollständige Zahlungsart-Integration, ohne dass eine einzige Kernklasse von Magento überschrieben werden muss.
Der entscheidende Vorteil gegenüber der alten AbstractMethod: Jede neue Anforderung, etwa ein zusätzlicher Zustandsübergang für Teilerstattungen, führt zu einem neuen Command statt zu einer weiteren Methode in einer wachsenden Basisklasse. Die Zahlungsart-Integration bleibt dadurch auch nach Jahren an Erweiterungen übersichtlich und einzeln testbar.
3. ConfigProvider: Konfiguration ins Frontend reichen
Der ConfigProvider ist die Brücke zwischen serverseitiger Konfiguration und dem Checkout-Frontend. Er implementiert ConfigProviderInterface mit einer einzigen Methode getConfig(), die ein verschachteltes Array zurückgibt, das später als JSON an das Frontend übergeben wird. Für eine Zahlungsart-Integration gehören hier typischerweise der öffentliche API-Schlüssel, der Sandbox- oder Live-Modus sowie unterstützte Kartentypen hinein, niemals aber geheime Zugangsdaten wie ein Secret Key, der ausschließlich serverseitig verwendet werden darf.
Diese Trennung ist sicherheitsrelevant: Alles, was der ConfigProvider zurückgibt, landet unverschlüsselt im HTML der Checkout-Seite und ist damit für jeden Browser-Client einsehbar. Eine Zahlungsart-Integration, die versehentlich einen Secret Key über den ConfigProvider ausliefert, öffnet damit ein ernstes Sicherheitsloch, unabhängig davon, wie gut der Rest der Integration abgesichert ist.
<?php
declare(strict_types=1);
namespace Mironsoft\PaymentGateway\Model;
use Magento\Checkout\Model\ConfigProviderInterface;
use Magento\Payment\Helper\Data as PaymentHelper;
use Magento\Payment\Model\MethodInterface;
/**
* Provides public, non-secret gateway configuration to the checkout frontend.
*/
class GatewayConfigProvider implements ConfigProviderInterface
{
private const METHOD_CODE = 'mironsoft_gateway';
private readonly MethodInterface $method;
/**
* @param PaymentHelper $paymentHelper
*/
public function __construct(
private readonly PaymentHelper $paymentHelper
) {
$this->method = $this->paymentHelper->getMethodInstance(self::METHOD_CODE);
}
/**
* Return only public configuration values, never secret credentials.
*
* @return array<string, mixed>
*/
public function getConfig(): array
{
if (!$this->method->isAvailable()) {
return [];
}
return [
'payment' => [
self::METHOD_CODE => [
'publicKey' => (string) $this->method->getConfigData('public_key'),
'sandbox' => (bool) $this->method->getConfigData('sandbox_mode'),
'supportedCardTypes' => explode(',', (string) $this->method->getConfigData('cctypes')),
],
],
];
}
}
4. Commands registrieren: Authorize, Capture, Cancel
Nach der Konfiguration folgt der eigentliche Kern jeder Zahlungsart-Integration: die Commands. Jeder Command kapselt genau einen HTTP-Aufruf gegen die API des Zahlungsdienstleisters und übersetzt Request und Response zwischen Magento-Datenstrukturen und dem externen Gateway-Format. Die Registrierung der Commands erfolgt deklarativ per di.xml über einen virtuellen Typ des CommandPool, ganz ohne Preferences auf Kernklassen.
Wichtig für eine saubere Zahlungsart-Integration: Die HTTP-Kommunikation selbst gehört nicht in den Command, sondern in einen separaten Client-Service, den der Command per Constructor Property Promotion injiziert bekommt. So bleibt der Command auf die Übersetzungslogik zwischen Magento und Gateway beschränkt, während der Client unabhängig getestet und bei Bedarf gegen eine andere HTTP-Bibliothek ausgetauscht werden kann.
<?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="MironsoftGatewayCommandPool" type="Magento\Payment\Gateway\Command\CommandPool">
<arguments>
<argument name="commands" xsi:type="array">
<item name="authorize" xsi:type="string">Mironsoft\PaymentGateway\Gateway\Command\AuthorizeCommand</item>
<item name="capture" xsi:type="string">Mironsoft\PaymentGateway\Gateway\Command\CaptureCommand</item>
<item name="cancel" xsi:type="string">Mironsoft\PaymentGateway\Gateway\Command\CancelCommand</item>
</argument>
</arguments>
</virtualType>
<virtualType name="MironsoftGatewayValueHandlerPool" type="Magento\Payment\Gateway\Config\ValueHandlerPool">
<arguments>
<argument name="handlers" xsi:type="array">
<item name="default" xsi:type="string">MironsoftGatewayConfigValueHandler</item>
</argument>
</arguments>
</virtualType>
</config>
Der AuthorizeCommand selbst führt den API-Aufruf aus, prüft die Antwort auf einen erfolgreichen Autorisierungscode und schreibt bei Erfolg die Gateway-Transaktions-ID als lastTransId auf das Payment-Objekt. Diese Transaktions-ID wird später von Capture und Cancel benötigt, um die ursprüngliche Autorisierung eindeutig zu referenzieren.
<?php
declare(strict_types=1);
namespace Mironsoft\PaymentGateway\Gateway\Command;
use Magento\Payment\Gateway\CommandInterface;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Magento\Payment\Gateway\Command\ResultInterface;
use Mironsoft\PaymentGateway\Gateway\Http\GatewayClientInterface;
/**
* Authorizes a payment against the external gateway and stores the
* resulting transaction id on the Magento payment for later capture calls.
*/
class AuthorizeCommand implements CommandInterface
{
/**
* @param GatewayClientInterface $client
*/
public function __construct(
private readonly GatewayClientInterface $client
) {
}
/**
* @param array{payment: PaymentDataObjectInterface, amount: float} $commandSubject
* @return ResultInterface|null
*/
public function execute(array $commandSubject): ?ResultInterface
{
/** @var PaymentDataObjectInterface $paymentDataObject */
$paymentDataObject = $commandSubject['payment'];
$payment = $paymentDataObject->getPayment();
$amount = (float) $commandSubject['amount'];
$response = $this->client->authorize([
'order_reference' => $paymentDataObject->getOrder()->getOrderIncrementId(),
'amount' => $amount,
'currency' => $paymentDataObject->getOrder()->getCurrencyCode(),
]);
$payment->setTransactionId($response['transaction_id']);
$payment->setIsTransactionClosed(false);
return null;
}
}
5. Value Handler und Response Validator für Gateway-Antworten
Ein Value Handler übersetzt einen einzelnen Konfigurationswert, etwa ob die Zahlungsart-Integration aktuell im Sandbox-Modus läuft, in einen Wert, den Magento an anderer Stelle abfragen kann, etwa im Admin-Grid oder in der Rechnungsansicht. Response Validatoren dagegen prüfen die komplette Antwort eines Gateway-Aufrufs und entscheiden, ob dieser als Erfolg oder Misserfolg gilt, unabhängig vom HTTP-Statuscode, der bei manchen Zahlungsdienstleistern auch bei fachlichen Fehlern 200 zurückgeben kann.
Diese Trennung ist entscheidend: Ein HTTP-200-Response mit dem Feld status: declined im Body ist aus Sicht der Zahlungsart-Integration ein fachlicher Fehlschlag, auch wenn die Transportebene erfolgreich war. Ein Response Validator, der nur den HTTP-Statuscode prüft, würde eine abgelehnte Zahlung fälschlicherweise als erfolgreich autorisiert markieren, mit entsprechend fatalen Folgen für den weiteren Bestellprozess.
<?php
declare(strict_types=1);
namespace Mironsoft\PaymentGateway\Gateway\Validator;
use Magento\Payment\Gateway\Validator\AbstractValidator;
use Magento\Payment\Gateway\Validator\ResultInterface;
use Magento\Payment\Gateway\Validator\ResultInterfaceFactory;
/**
* Validates the business level status inside a gateway response, independent
* of the HTTP transport status which may be 200 even for declined payments.
*/
class AuthorizeResponseValidator extends AbstractValidator
{
/**
* @param ResultInterfaceFactory $resultFactory
*/
public function __construct(
private readonly ResultInterfaceFactory $resultFactory
) {
parent::__construct($resultFactory);
}
/**
* @param array{response: array<string, mixed>} $validationSubject
* @return ResultInterface
*/
public function validate(array $validationSubject): ResultInterface
{
$response = $validationSubject['response'];
$isValid = ($response['status'] ?? null) === 'approved';
$fails = [];
if (!$isValid) {
$fails[] = sprintf('Gateway lehnte die Autorisierung ab: %s', $response['decline_reason'] ?? 'unbekannt');
}
return $this->createResult($isValid, $fails);
}
}
6. Die Zahlungsart im Hyvä-Checkout-Frontend rendern
Der letzte Schritt jeder Zahlungsart-Integration ist ihre Darstellung im Checkout selbst. In einem Luma-Setup wäre das eine KnockoutJS-Komponente, die im Payment-Step als weiterer Renderer registriert wird. In einem Hyvä-Projekt entfällt dieser Weg vollständig: Die Zahlungsart wird als Magewire-Komponente implementiert, die den Namen der Methode, ihr Logo und ein Alpine.js-Template für eventuelle Zusatzfelder rendert, etwa ein Feld für eine Bestellreferenz beim Rechnungskauf.
Die Auswahl der Zahlungsart selbst löst serverseitig über Magewire einen Aufruf aus, der die gewählte Methode auf die Quote setzt, ohne dass ein zusätzlicher REST-Request aus dem Frontend nötig wird. Das reduziert die Anzahl der Netzwerk-Roundtrips im Vergleich zum klassischen Knockout-Checkout, bei dem Payment-Auswahl und spätere Bestellaufgabe typischerweise getrennte Requests sind, spürbar.
7. Asynchrone Statusupdates per Webhook verarbeiten
Viele Zahlungsdienstleister bestätigen eine Zahlung nicht synchron im ursprünglichen Checkout-Request, sondern melden den finalen Status später per Webhook, etwa wenn eine Sofortüberweisung erst nach mehreren Sekunden bestätigt wird oder eine Rückbuchung Tage später eintrifft. Eine vollständige Zahlungsart-Integration braucht deshalb einen eigenen Controller, der Webhook-Aufrufe entgegennimmt, die Signatur des Anbieters verifiziert und den Bestellstatus entsprechend aktualisiert.
Die Signaturprüfung ist dabei nicht optional: Ohne sie könnte jeder, der die Webhook-URL kennt, beliebige Zahlungsstatus-Updates einschleusen und etwa eine nicht bezahlte Bestellung künstlich als bezahlt markieren. Der Webhook-Controller sollte deshalb konsequent den vom Zahlungsdienstleister mitgesendeten HMAC-Header gegen den eigenen Secret Key prüfen, bevor irgendeine Statusänderung an der Order vorgenommen wird.
#!/usr/bin/env bash
# Local test call against the webhook endpoint with a valid HMAC signature
set -euo pipefail
PAYLOAD='{"order_reference":"000000123","status":"captured","transaction_id":"tx_9f8e7d"}'
SECRET="whsec_test_only_do_not_use_in_production"
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
curl -s -X POST "https://shop.example.com/mironsoft_gateway/webhook/notify" \
-H "Content-Type: application/json" \
-H "X-Gateway-Signature: $SIGNATURE" \
-d "$PAYLOAD"
8. Fehlerbehandlung und Idempotenz bei Zahlungsanfragen
Netzwerke sind unzuverlässig, und eine Zahlungsart-Integration muss damit rechnen, dass ein Request zwar beim Gateway ankommt, die Antwort aber wegen eines Timeouts nie bei Magento eintrifft. Ohne Idempotenz-Schutz würde ein automatischer Retry in diesem Fall eine zweite Autorisierung auslösen und den Kunden doppelt belasten. Die Lösung ist ein eindeutiger Idempotency-Key pro Bestellversuch, den der Gateway-Client bei jedem Request mitsendet, sodass der Zahlungsdienstleister einen wiederholten Request mit demselben Key als Duplikat erkennt und die ursprüngliche Antwort erneut zurückgibt, statt die Zahlung ein zweites Mal auszuführen.
Zusätzlich sollte jeder Command in der Zahlungsart-Integration Netzwerkfehler explizit von fachlichen Ablehnungen unterscheiden. Ein Verbindungsabbruch sollte eine CommandException auslösen, die der Checkout dem Kunden als temporären Fehler mit Retry-Option präsentiert, während eine fachliche Ablehnung, etwa wegen unzureichender Deckung, als endgültiges Ergebnis mit klarer Fehlermeldung behandelt wird. Wer beide Fälle gleich behandelt, verwirrt Kunden entweder mit einer Retry-Option bei endgültig abgelehnten Zahlungen oder mit einer endgültigen Fehlermeldung bei einem temporären Netzwerkproblem.
9. Eigene Integration im Vergleich zur Payment-Bridge
Nicht jedes Projekt braucht eine vollständig eigene Zahlungsart-Integration. Für etablierte Zahlungsdienstleister existieren oft fertige Magento-Extensions, die dasselbe Command Pattern bereits implementieren. Die folgende Tabelle zeigt, wann sich der Eigenbau lohnt.
| Kriterium | Fertige Payment-Extension | Eigene Zahlungsart-Integration | Empfehlung |
|---|---|---|---|
| Etablierter Anbieter (PayPal, Adyen) | Verfügbar, gepflegt | Hoher Aufwand ohne Mehrwert | Fertige Extension nutzen |
| Nischen- oder lokaler Anbieter | Oft nicht verfügbar | Notwendig | Eigene Integration bauen |
| Volle Kontrolle über Fehlerbehandlung | Abhängig vom Extension-Hersteller | Vollständig steuerbar | Eigenbau bei speziellen Anforderungen |
| Wartungsaufwand über Zeit | Trägt der Hersteller | Trägt das eigene Team | Ressourcen realistisch einplanen |
In der Praxis liegt die richtige Entscheidung selten an den technischen Möglichkeiten, sondern an der langfristigen Wartungsverantwortung. Eine eigene Zahlungsart-Integration für einen etablierten globalen Anbieter bindet Entwicklungszeit, die eine fertige, regelmäßig aktualisierte Extension meist günstiger und zuverlässiger abdeckt.
Mironsoft
Magento 2 Payment-Gateway-Entwicklung und Checkout-Integration
Eine eigene Zahlungsart-Integration für euren Checkout?
Wir bauen vollständige Zahlungsart-Integrationen nach dem Payment Gateway Command Pattern, von Commands über Webhook-Verarbeitung bis zur Anbindung an euer Hyvä-Checkout-Frontend.
Command Pattern
Authorize, Capture und Cancel als saubere, einzeln testbare Commands
Webhook-Handling
Signaturprüfung und Idempotenz für asynchrone Statusupdates
Hyvä-Frontend
Magewire-Komponente statt Knockout-Renderer für die Zahlungsart
10. Zusammenfassung
Eine solide Zahlungsart-Integration in Magento 2 basiert auf dem Payment Gateway Command Pattern: ConfigProvider für Frontend-Konfiguration, CommandPool für Authorize, Capture und Cancel, Value Handler und Response Validator für Gateway-Antworten. Jeder Baustein bleibt klein, einzeln testbar und wird ausschließlich per di.xml registriert, ohne eine einzige Kernklasse zu überschreiben.
Zwei Aspekte entscheiden in der Praxis über die Zuverlässigkeit jeder Zahlungsart-Integration: eine korrekt verifizierte Webhook-Verarbeitung für asynchrone Statusupdates und ein konsequenter Idempotenz-Schutz gegen doppelte Belastungen bei Netzwerkfehlern. Wer diese beiden Punkte von Anfang an mitdenkt, baut eine Zahlungsart-Integration, die auch unter Last und bei unzuverlässigen Netzwerken korrekt bleibt.
Eigene Zahlungsart-Integration in Magento 2, das Wichtigste auf einen Blick
Architektur
ConfigProvider, CommandPool, Value Handler und Response Validator ersetzen die alte AbstractMethod-Klasse.
Sicherheit
Niemals Secret Keys über den ConfigProvider ausliefern, Webhook-Signaturen konsequent verifizieren.
Zuverlässigkeit
Idempotency-Keys verhindern doppelte Belastungen bei Netzwerkfehlern und automatischen Retries.
Hyvä-Frontend
Magewire-Komponente statt Knockout-Renderer, ein serverseitiger Aufruf statt getrennter REST-Requests.