collana pay Integration Guideline
Dieser Artikel beschreibt die Integration von collana pay in ein Shopsystem. Es stellt keine spezifische Anleitung für ein konkretes Shopsystem dar, sondern zeigt die allgemeinen Prinzipien und mögliche Lösungswege auf. Im zweiten Teil dieser Guideline wird exemplarisch anhand der Shopsoftware "Shopware" eine mögliche konkrete Implementierung aufgezeigt.
Vorrangiges Ziel dieses Artikels ist es jedoch, dem Entwickler die Arbeitsweise von collana pay, seine Vorteile gegenüber anderen Lösungen und Best-Practise-Verfahren zur Nutzung näherzubringen.
collana pay
Die zugrunde liegende Idee von collana pay ist es, eine allgemeine Schnittstelle zu verschiedenen Payment-Service-Providern (im folgenden PSPs) anzubieten und so von spezifischen Eigenheiten verschiedener PSPs unabhängig zu werden. Dieser Ansatz bietet dem Shopbbetreiber zwei unmittelbare Vorteile:
- Die Anbindung eines weiteren oder eines anderen PSP ist sehr einfach, da die selbe API weiterhin verwendet werden kann. Die Auswahl einer konkreten Zahlart erfolgt über ein HTTP-Header-Feld. Ein Wechsel des PSP bei gleicher Zahlart erfordert keinerlei Änderungen im Shopsystem. Der PSP der Zahlart wird in collana pay festgelegt.
- Änderungen an den APIs der PSP müssen nicht zwangsläufig in die Integration im Shopsystem übertragen werden. collana pay arbeitet als Middleware und übernimmt diese Aufgabe. Dadurch ist die Verwendung von collana pay deutlich wartungsärmer als direkte Integrationen.
Ohne collana pay müssen Onlineshops mehrere PSP anbinden bzw. müssen beim Wechsel zwischen PSP auch die Integration wechseln:
collana pay übernimmt diese Aufgabe, so dass Onlineshops lediglich eine einzige Schnittstelle integrieren müssen.
Asynchronität
Ein spezifisches Merkmal von collana pay ist die Asynchronität, die im Rahmen elektronischer Zahlungsverarbeitung den bereits etablierten Gedanken von IPN (Instant Payment Notification) weitertreibt und die gesamte Kommunikation darauf aufbaut. IPN basiert auf der optionalen Möglichkeit, zusätzlich zu einer synchronen Kommunikation einen asynchronen Kanal aufzubauen. Über diesen können PSPs weitere Informationen, wie etwa Aktualisierungen und Statusmeldungen, an den Onlineshop senden. Als Weiterentwicklung ist die Asynchronität bei collana pay nun nicht mehr eine Option, sondern die Basis der Kommunikation. Auch wenn dies in manchen Bereichen ein Bruch mit bekannten Verfahren ist, so bringt diese Architektur einige Vorteile mit sich.
Asynchrone Systeme sind prinzipiell robuster, da sie nicht wie klassische synchrone Kommunikation auf stabile Netzwerkverbindungen angewiesen sind. Hier findet eine Umkehr der Abhängigkeit statt, so dass Netzwerkprobleme nicht gesondert behandelt werden müssen, sondern nur auf eingehende Anfragen reagiert wird.
Desweiteren unterstützen asynchrone Systeme den zu beobachtenden Trend zu Microservices, der ohnehin eine striktere und robustere Trennung einzelner Systemkomponenten propagiert. Gerade im Web-Umfeld mit dem Fortschreiten moderner Lösungen wie PWA und Websockets spielen asynchrone Verfahren ihre Stärken aus.
REST-API
Allgemein
collana pay stellt eine generische REST-API bereit, die unabhängig von den dahinter liegenden konkreten PSPs arbeitet und die eingangs erwähnte Flexibilität ermöglicht.
Dokumentation
Die Dokumentation der API wird von der collana hive via OpenAPI beschrieben und online über Swagger oder ReDoc bereitgestellt.
https://sandbox.collanapay.com/swagger/index.html#/
https://sandbox.collanapay.com/redoc/index.html?url=/swagger/v2/swagger.json
Verwendung
Die REST-API arbeitet mit den bekannten HTTP-Verben. Die Authentifizierung erfolgt über einen API-Key, der bei jeder Anfrage im HTTP-Header mitgesendet wird. Als Transportformat wird JSON verwendet. JSON zeichnet sich gegenüber XML durch einen geringeren Overhead und der nativen Verfügbarkeit verschiedener Datentypen aus.
Entwicklung
Da REST auf einfachen, etablierten Technologien aufbaut, kann die API prinzipiell mit einfachen Werkzeugen wie curl
auf dem Terminal angesprochen werden. Es gibt jedoch einige Werkzeuge, die die Arbeit während der Entwicklung deutlich vereinfachen können.
Wir empfehlen die Verwendung folgender Werkzeuge:
- Postman ist ein komfortabler HTTP-Client, der speziell für die Arbeit mit APIs vorgesehen ist. Mehr unter https://www.postman.com
- Webhook.Site ist ein Dienst, um eingehende HTTP-Requests zu analysieren und ist gerade für die Arbeit mit asynchronen Systemen ideal. Mehr unter https://webhook.site
Auch wenn wir diese Werkzeuge verwenden, so sind sie nicht notwendig, um die Integration von collana pay vorzunehmen.
Verfahren
Unabhängig von dem eingesetzten Shopsystem und daraus folgend der Programmiersprache, in welcher es geschrieben ist, werden im Folgenden die benötigten Teilschritte aufgezeigt, die zu Implementierung notwendig sind.
Dennoch werden zwischendurch Verweise zum Implementierungsbeispiel gegeben.
Ablauf
Ganz allgemein gilt folgender Ablauf:
- Der Kunde entscheidet sich für Abwicklung der Zahlung mit collana pay. Dabei ist es nicht notwendig, dass collana pay für den Kunden sichtbar wird, eher wird für den Kunden die Zahlart einfach als Zahlungsabwicklung dargestellt.
- Das Shopsystem kommuniziert mit dem collana pay via REST und erstellt dort eine neue Transaktion. Als Antwort erhält das Shopsystem eine eindeutige
transactionId
. - Das Shopsystem kann nun mit dieser Transaktions-ID mit collana pay kommunizieren und weitere Schritte auslösen, die als Interaktionen bezeichnet werden. Jede Interaktion wird durch eine eindeutige
interactionId
identifiziert. Die Antworten darauf erhält das Shopsystem asynchron, diese sind immer mit der zuvor erstellten übergreifendentransactionId
sowie der eigeneninteractionId
gekennzeichnet. - Die weiteren Aktionen des Kunden finden nun nicht mehr mit dem Shopsystem, sondern mit collana pay bzw. dem PSP statt. Die Antwort von collana pay enthält eine URL. Zu dieser wird der Kunde umgeleitet. Der Kunde gibt dort die zur Verarbeitung notwendigen Daten (z.B. Kreditkartendaten) ein und wird zurück zum Shop geleitet.
- Das Shopsystem verarbeitet die asynchrone Antwort auf die Eingabe des Kunden bei collana pay.
Dabei kommt es nicht darauf an, an welcher Stelle das Shopsystem die Abwicklung der Zahlung im Checkout-Prozess verortet. Dieser Prozess ist in sich geschlossen.
Begrifflichkeiten
Transaktion
Die Transaktion bezeichnet die vollständige Abwicklung einer Zahlung innerhalb von collana pay. Auch wenn mehrere Schritte durchgeführt und verschiedene Teilaufgaben durchgeführt werden müssen bzw. können, laufen diese alle unter einer übergreifenden Transaktions-ID ab.
Interaktion
Eine Transaktion besteht aus mehreren Teilschritten, den sogenannten Interaktionen. Ein Beispiel für eine Interaktion ist etwa eine reservation
. Jede Interaktion wird durch eine eigene interactionId
gekennzeichnet.
Frontend
Die genaue Definition des Begriffs "Frontend" (wie auch bei seinem Gegenstück "Backend") ist nicht einheitlich und unterscheidet sich je nach Umgebung und Anwendungsart. Für den Kontext dieses Dokuments bezeichnen wir mit Frontend den clientseitig ausgelieferten und ausgeführten Code einer E-Commerce-Lösung.
In den meisten Fällen wird dies ein (zumindest teilweise) serverseitig gerendertes HTML-Dokument sein, welches mithilfe von Scriptsprachen, zumeist JavaScript, verschiedenen Aufgaben (vor allem UI-Logik & Validierung, aber auch Kommunikation) durchführen kann.
Das Frontend unterliegt nicht der unmittelbaren Kontrolle des Anbieters, sondern wird vollständig in der Umgebung des Nutzers, d.h. meist ein Webbrowser, ausgeführt. Aus diesem Grund müssen Entwickler im Frontend einige Besonderheiten beachten. Diese sind nicht spezifisch für collana pay, sondern allgemein geboten:
- Die Verfügbarkeit von Fähigkeiten kann nur sehr eingeschränkt angenommen werden. Entwickler sollten für sich bzw. in Abstimmung mit dem Auftraggeber oder Betreiber ein Minimum an Anforderungen definieren, dieses kann dem Endanwender dann auch kommuniziert werden. Wichtig ist hierbei, eine Balance zwischen breiter Akzeptanz und Entwicklungs- & Wartungsaufwand zu finden. Eine Anwendung, die vollständig auf Javascript verzichtet, wird zwar ohne Probleme auf jedem Endgerät funktionieren, aber viel Komfort vermissen lassen.
- Die Stabilität der Anwendung unterliegt deutlich größeren Unsicherheiten, zudem liegen anders als beim serverseitigen Teil oftmals deutlich weniger Möglichkeiten zum Logging & Debugging vor. Insbesondere die Laufzeitumgebung muss unter völlig anderen Gesichtspunkten betrachtet werden. Verfügbare Ressourcen (insb. Prozessorleistung), Netzwerkleistung (Bandbreite), und ähnliches unterliegen starken Schwankungen und müssen deutlich intensiver getestet werden.
- Die Sicherheit der Anwendung kann nicht gewährleistet werden. Alle vom Client empfangenen Daten müssen als unsicher betrachtet werden. Dazu gehören nicht nur die häufig geprüften Daten, die via POST oder als GET-Parameter empfangen werden, auch Daten aus dem HTTP-Header, insbesondere Cookies, können leicht manipuliert werden. Clientseitige Validierung kann verwendet werden, um die Nutzererfahrung zu verbessern und die serverseitige Anwendung zu entlasten, ersetzt aber niemals eine serverseitige Prüfung. Hierbei muss auch auf den oberen Punkt der Verfügbarkeit geachtet werden. Die Verwendung moderner HTML5-Input-Typen wie
date
odernumber
oder funktionale Attribute wierequired
werden u.U. überhaupt nicht interpretiert.
Daher ein wichtiger Hinweis: Aus Gründen der Übersichtlichkeit verzichten wir in diesem Dokument auf viele sicherheitskritische Prüfung, Fehlerbehandlungen oder stabilisierende Fallback- und Polyfill-Funktionen. Entwickler sind angehalten, abhängig von ihrer eigenen Umgebung, Vereinbarungen und Anforderungen, diese selbst zu implementieren.
Backend
Analog zur vorherigen Definition des Frontends bezeichnen wir mit Backend die serverseitige Applikation einer E-Commerce-Lösung. Das Backend führt die Businesslogik durch und kommuniziert mit dem Frontend, indem es zum einen dasselbe bereitstellt und zum anderen URL-basierte Schnittstellen anbietet, von denen das Frontend Daten abholen bzw. an welche es Daten senden kann.
Viele Shopsysteme bezeichnen den Administrationsbereich des Systems oftmals als Backend. Diese Interpretation verwenden wir hier nicht, erwähnen diese aber hier explizit, um Unklarheiten zu vermeiden. Die konkreten Fähigkeiten des eingesetzten Shopsystems und Funktionen zur Konfiguration betrachten wir in diesem Dokument nicht.
Integration von collana pay als Zahlungsart
Das folgende Ablaufdiagramm liefert einen Überblick über den grundlegenden Ablauf einer Zahlung mithilfe von collana pay, unter Einbeziehung aller drei Komponenten (Frontend, Backend, collana pay):
Die Schritte im Detail
Nach dieser theoretischen Betrachtung folgen nun die konkreten einzelnen Schritte, aus denen eine Verarbeitung einer Zahlung über collana pay besteht:
In dieser Darstellung werden Frontend & Backend als gemeinsamer Akteur "Shop" betrachtet.
Wie zu sehen ist, werden insgesamt 3 Schritte durchgeführt, um die Zahlungsabwicklung im Shop durchzuführen. Diese sind im Detail:
create
Route: /transactions
Wird verwendet, um eine Transaktion zu erstellen.
-
Synchron wird die
transactionId
und eineinteractionId
zurückgeliefert. - Asynchron wird die Bestätigung & Zusammenfassung der Transaktion zurückgeliefert.
prepare
Route: /transactions/{transactionId}/prepares
Wird verwendet, um eine Zahlung vorzubereiten.
-
Synchron wird die
transactionId
und eineinteractionId
zurückgeliefert. -
Asynchron wird die Bestätigung & Zusammenfassung der Transaktion zurückgeliefert, zudem u.U. eine
RedirectUri
oder eineEmbedmentUri
, die dem Endkunden angezeigt bzw. zu der er weitergeleitet werden soll.
reservation
Route: /transactions/{transactionId}/reservations
Wird verwendet, um die Zahlung zu reservieren.
-
Synchron wird die
transactionId
und eineinteractionId
zurückgeliefert. -
Asynchron wird die Bestätigung & Zusammenfassung der Transaktion zurückgeliefert, zudem u.U. eine
RedirectUri
oder eineEmpedmentUri
, die dem Endkunden angezeigt bzw. zu der er weitergeleitet werden soll.
Weitere Aktionen
Wie in der Dokumentation ersichtlich ist, gibt es weitere Routen für weitere Aufgaben. Diese können aber prinzipiell auch von ERP-Systemen oder vom Shopsystem im nachgelagerten Order-Prozess durchgeführt werden und sind somit nicht zwingend notwendig für die Zahlungsverarbeitung innerhalb des Bestellprozess im Onlineshop. Daher werden diese hier nicht behandelt.
Sie arbeiten aber alle nach dem hier beschriebenen Verfahren und können daher ebenso einfach implementiert werden.
Erstellen einer Transaktion
Dies ist der Schritt, an dem auch die Verarbeitung "klassischer" Zahlarten beginnt. Der Nutzer hat im Frontend die Zahlungsart ausgewählt und durch eine beliebig gelagerte Interaktion, z.b. per Klick auf einen "zahlen"-Button, die Durchführung der Zahlung angestoßen. Im Backend beginnt nun die Verarbeitung der Transaktion.
Durch den Aufbau von collana pay und die Verwendung asynchroner Kommunikation ist es erforderlich, einen Weg zu schaffen, asynchrone Antworten global verfolgen und zuordnen zu können. Diese Aufgabe übernimmt die transactionId
, die nun geschaffen werden muss. Dies erfolgt über einen POST-Request an /transactions
:
curl -H "Content-Type: application/json"
-H "Accept: application/json"
-H "ApiKey: xxxx-yyyy-zzzz-0000"
-H "x-payment-method-type: CreditCard"
-d '{"key1":"value1", "key2":"value2"}'
-X POST https://collana pay/api/v2/transactions
Betrachten wir den Request etwas detaillierter:
-H "Content-Type: application/json"
-H "Accept: application/json"
Hier wird definiert, dass die übertragenden Daten in Form von JSON gesendet werden (Content-Type
) und die Antwort von collana pay auch in diesem Format erwartet wird (Accept
).
-H "ApiKey: xxxx-yyyy-zzzz-0000"
Dies ist der Login, mit dem das Backend sich bei collana pay authentifiziert. Der API-Key wird von der collana hive bereitgestellt.
-H "x-payment-method-type: CreditCard"
Dieser Header definiert die Zahlart. Die Zahlart ist ein fest definierter Wert (z.B. CreditCard
, PayPal
, PayPalExpress
, KlarnaSofort
, DirectDebit
, Terminal
, IDeal
, Invoice
) der PSP unabhängig ist. Die vollständige Liste wird von der collana hive bereitgestellt.
-d '{"key1":"value1", "key2":"value2"}'
Dies ist der Inhalt des HTTP-Body ("Payload"), der via POST gesendet wird. Die genaue Definition der Payload kann der Dokumentation (siehe "REST-API, Dokumentation") entnommen werden.
Die Antwort dieses Requests enthält die transactionId
, die für alle weitere Kommunikation notwendig ist. Daher ist es notwendig, diese in einer Form innerhalb des Shopsystems zu hinterlegen, mit der sie global verfügbar ist, z.B. in einer eigenen Datenbanktabelle. Im Implementierungsbeispiel wird dazu die Tabelle shopware.flenocollanapay_payments
verwendet.
Dieser Schritt kann synchronverarbeitet werden, d.h. die Antwort auf die Request kann direkt verarbeitet werden, da hierbei noch nicht mit dem PSP kommuniziert wird und eine umfassende Validierung die übermittelten Daten prüft. Kommt es also zu keinem HTTP-Fehler, war die Anlage erfolgreich. Der Vollständigkeit halber empfehlen wir aber, wie bei den anderen Calls auch hier auf die asynchrone Antwort zu warten und diese Statusabhängig zu verarbeiten.
Verarbeiten einer Transaktion
Im folgenden Schritt wird nun die tatsächliche Abwicklung der Zahlung gestartet. Hier müssen nun mehrere beteiligte Komponenten in Einklang gebracht werden:
- Das Frontend, d.h. der Browser des Kunden
- Das Backend, d.h. das Shopsystem
- collana pay
Klassischerweise erfolgt die Kommunikation zwischen 1. und 2. synchron, d.h. eine Aktion des Nutzers löst eine Reaktion des Shopsystems aus, der mit einer Antwort, meist eine neue Ressource oder ein Redirect, antwortet. Daneben kommunizieren 2. und 3. nun asychron, d.h. das Shopsystem sendet einen Request an collana pay, erhält die Antwort aber asynchron über eine Callback-URL. Die synchrone Antwort enthält lediglich die Information, ob die Anfrage angenommen wurde, sowie bereits erwähnt die transactionId
& interactionId
.
Um dies aufzulösen, empfehlen wir den Aufbau einer zweigeteilten Architektur, die das Frontend in den Mittelpunkt stellt. Dazu wird der Nutzer nach Abschluss von Schritt 1 auf eine Seite des Shopsystems geleitet, auf der alle weiteren Kommunikationsschritte koordiniert werden. Parallel dazu baut sich das Shopsystem eine eigene interne "Middleware" auf, die den aktuellen Zustand der Zahlung repräsentiert, und das Frontend von collana pay abschirmt.
Somit existiert keine unmittelbare Abhängigkeit zwischen dem Frontend und collana pay. Vielmehr stellt das Backend eine Möglichkeit bereit, aus der asynchronen Kommunikation mit collana pay eine synchrone Repräsentation für das Frontend abzuleiten. Dieses wird wiederum quasi in Echtzeit Wechsel der Zustände abbilden, wofür wiederum auf asynchrone Technologien zurückgegriffen wird.
Hierzu baut das Frontend eine asynchrone Verbindung zu einem lokalen Endpunkt des Backends auf. Aus Gründen der Kompatibilität kommt hierbei am ehesten eine Longpolling-Lösung in Frage. Ideal wäre eine auf Websockets basierende Architektur, doch würde dies für die meisten gängigen Shopsysteme und ihre Serverarchitektur einen zu großen Umbau bedeuten.
Integration im Frontend
Das Frontend baut via Longpolling eine Verbindung zu einem Endpunkt des Backends auf. Die Aufgabe dieser Verbindung ist es, den aktuellen Zustand der Transaktion zwischen Frontend und Backend zu synchronisieren.
Integration im Backend
Das Backend stellt zwei Endpunkte zur Verfügung, die zum lesen (GET
) und zum schreiben (POST
) genutzt werden.
Der GET
-Endpunkt dient dem Frontend zur Kommunikation und stellt im Wesentlichen die Information über den aktuellen Zustand der Transaktion dar. Der POST
-Endpunkt dient collana pay als Ziel für asynchrone Responses.
Zusätzlich verfügt das Backend über ein Verfahren zur persistenten Speicherung des Zustands der Zahlung. Der Zustand einer Zahlung besteht im Wesentlichen aus 3 Informationen:
- die eindeutige Kennung (
transactionId
) - einen Code zur Identifikation des Zustands in Abhängigkeit zur Interaktion (
status
) - einer je nach aktuellem Zustand nötigen payload, z.B. einer Redirect-URL.
In den meisten Fällen wird hierfür eine Datenbanktabelle verwendet werden, auch dieser Leitfaden geht in seinen Code-Beispielen davon aus.
Im ersten Schritt wurde die Transaktion erstellt, d.h. die Transaktion befindet sich im Zustand create.done
. Diese Benennung ist rein willkürlich und kann vom Entwickler angepasst werden, sie dient hier nur exemplarisch.
Zusammenspiel der Komponenten
Nun betrachten wir, wie die eingangs genannten 3 Komponenten (Frontend, Backend, collana pay) miteinander interagieren.
Fortschreiten im Frontend
Das Frontend kommuniziert dauerhaft mit dem Backend. Es hat Kenntnis darüber, wie es bei jedem Zustand zu (re)agieren hat. Damit erreichen wir zugleich, dass der Nutzer dauerhaft über den aktuellen Zustand seiner Zahlungsverarbeitung informiert bleibt und nicht das Gefühl erhält, das System wäre zäh oder gar abgestürzt.
Das Frontend erhält unmittelbar die Information, dass die Transaktion im Zustand create.done
ist, und kommuniziert mit dem Backend via AJAX, um den aktuellen Zustand darzustellen. Das Backend wiederum kommuniziert mit collana pay und hinterlegt die jeweiligen Stadien im aktuellen lokalen Zustand.
Aktivität des Backend
Nach dem Erstellen der Transaktion und der Verarbeitung der Response hat das Backend den lokalen Zustand angelegt. Außerdem hat es direkt den nächsten Schritt der Verarbeitung (prepare
) angestoßen. Dazu stellt es die nächste Anfrage an collana pay und fordert das System auf, die Zahlung vorzubereiten:
curl -H "Content-Type: application/json"
-H "Accept: application/json"
-H "ApiKey: xxxx-yyyy-zzzz-0000"
-H "x-payment-method-type: CreditCard"
-d '{"key1":"value1", "key2":"value2"}'
-X POST https://collanapay/api/v2/transactions/1234-5678-9101-2131/prepares
Die einzelnen Bestandteile des Requests wurde bereits behandelt. Zu beachten ist diesmal insbesondere das Ziel des Requests:
-X POST https://collanapay/api/v2/transactions/1234-5678-9101-2131/prepares
Neben der Payload im HTTP-Body (-d
) werden nun auch Daten in der URL übertragen. Dies erfolgt innerhalb der Ressource, nicht als query
-Parameter. Die URL beinhaltet also einen dynamischen Part zur Übertragung der transactionId
: https:/collanapay/api/v2/transactions/{transactionId}/prepares
. Dieses Verfahren wird auch für alle weiteren Routen (z.B. reservations
, captures
usw.) verwendet.
collana pay kann auf diesen HTTP-Request nun auf zwei Arten reagieren:
- Die Antwort ist ein Success-Code. Dann wurde die Anfrage angenommen, und collana pay wird die Antwort an die zuvor definierte Callback-Adresse senden.
- Die Antwort ist ein Fehlercode. Dieser muss interpretiert werden. Das Ergebnis kann entweder lauten, es muss ein neuer Versuch unternommen werden, z.B. bei Netzwerkproblemen. Dann wird der Zustand einfach zurückgesetzt auf den vorherigen, und ein neuer Versuch wird automatisch durchgeführt. Oder das Ergebnis weist auf größere Probleme hin, dann muss die Transaktion abgebrochen werden, und der Kunde muss eine andere Zahlungsart verwenden.
Je nach Antwort von collana pay wird nun der neue Zustand lokal hinterlegt und lautet entweder prepare.send
oder prepare.error
.
Das Frontend hat unterdessen den neuen Zustand erhalten. Bei prepare.send
kann z.B. ein Fortschrittbalken aktualisiert werden, oder eine Textmeldung angezeigt werden. Bei error
muss der Kunde darüber informiert und zurück zur Auswahl der Zahlungsart geleitet werden.
Der nächste Schritt
Zwischenzeitlich hat collana pay den prepare
-Schritt vorgenommen und das Shopsystem darüber über die Callback-URL informiert. Dies löst dann eine Aktualisierung des Zustandes aus, der nun prepare.pending
lautet. Das Frontend erhält diese Information, und agiert genau so wie im vorherigen Abschnitt beschrieben.
Zusätzlich hat collana pay eine URL übermittelt. Diese stellt eine Eingabemaske für den Kunden bereit, in welcher dieser seine Daten eintragen muss. Diese URL wird ebenfalls im lokalen Zustand hinterlegt und dem Frontend übermittelt, welches die URL in einem iFrame darstellt oder zur entsprechenden URL weiterleitet.
Reaktion des Frontend
Das Frontend hat, wie bereits beschrieben, in Quasi-Echtzeit Kenntnis über den Zustand der Zahlung. Der nun vorliegende Zustand beinhaltet die übermittelte URL, welche das Frontend in einem Iframe anzeigt, während parallel der Abgleich des Zustands ganz normal weiterläuft.
Wenn der Nutzer im Frontend innerhalb des Iframes seine Daten eingegeben und abgesendet hat, wird collana pay das Backend über die bekannte Callback-Adresse darüber informieren. Das Backend wird den Zustand der Zahlung aktualisieren, das Frontend wird darüber auf bekannten Weg informiert.
Im weiteren Verlauf wird nach dem Schritt reservation
eine Redirect-URL von collana pay an das Backend übermittelt, welche an das Frontend weitergegeben wird. Dieses wird den Kunden an die URl weiterleiten, womit das Verarbeiten der Zahlung aufseiten des Frontends abgeschlossen ist.
Abschluss einer Transaktion
Der Kunde wurde im vorherigen Schritt an collana pay weitergeleitet, welches dann die weitere Verarbeitung der Zahlung übernimmt. Nach Abschluss der Verarbeitung wird der Kunde an die zuvor definierte Redirect-URL des Shops zurückgeleitet.
Dies ist sinnvollerweise die bereits bekannte Seite des Frontends, die nun wieder ihre Aufgabe übernimmt, den Kunden über den aktuellen Zustand der Zahlungsverarbeitung zu informieren.
Parallel dazu erhält das Backend von collana pay die asynchron übermittelte Information, dass die Zahlung abgeschlossen ist.
Implementierung
Nach dieser theoretischen Betrachtung des Verfahrens wird dies nun anhand einer reellen Implementierung verdeutlicht. Zugrunde liegt ein Payment-Plugin für das Shopsystem Shopware5.
Bitte beachten Sie, dass der hier gezeigte Code nicht vollständig ist. Er wurde verkürzt, um den Fokus auf das collana pay-Verfahren zu legen.
Backend
Wie genau die Initiierung des Zahlungsvorgangs beginnt, ist abhängig von der verwendeten Software. In diesem Beispiel wird zur Veranschaulichung Code aus der Implementierung für das Shopsystem Shopware verwendet, in der eine Zahlung mit dem Aufruf einer Controller-Route (s.u.) beginnt:
class Shopware_Controllers_Frontend_CollanaPay extends \Shopware_Controllers_Frontend_Payment
{
public function createAction()
{
$body = [
'amount' => $this->getAmount(),
'currencyCode' => 'EUR',
'paymentCountry' => 'DE',
'billingCustomerData' => $this->getAddress(),
'shippingCustomerData' => $this->getShippingAddress(),
'baseUrl' => Shopware()->Shop()->getBaseUrl() . $this->Front()->Router()->assemble([
'controller' => 'collanapay',
'action' => 'return'
])
];
$response = \FlenoCollanaPay\Api::CreateTransaction($body);
$payment = new \FlenoCollanaPay\Models\Payment\Payment();
$payment->transactionId = $response['transactionId'];
$entityManager = $this->container->get('models');
$entityManager->persist($payment);
$entityManager->flush();
return $this->redirect($this->Front()->Router()->assemble([
'controller' => 'collanapay',
'action' => 'process'
]);
}
}
Dies erstellt eine neue Zahlung und hinterlegt die transactionId
in einer Datenbanktabelle.
Anmerkung: Das Shopsystem Shopware verwendet das MVC-Muster. Dieses ist nicht Teil dieses Leitfadens, zum besseren Verständnis dennoch ein kurzer Überblick. MVC (Model View Controller) ist ein Muster zur Unterteilung einer Software in die drei Komponenten Datenmodell (model), Präsentation (view) und Programmsteuerung (controller). Die drei Komponenten hängen je nach Umsetzung unterschiedlich stark voneinander ab:
- Model - Das Modell enthält Daten, die von der Präsentation dargestellt werden. Es ist von Präsentation und Steuerung unabhängig. Die Änderungen der Daten werden der Präsentation durch das Entwurfsmuster „Beobachter“ bekanntgegeben. In manchen Umsetzungen des MVC-Musters enthält das Modell eine Geschäftslogik, die für die Änderung der Daten zuständig ist.
- View - Die Präsentation ist für die Darstellung der Daten des Modells und die Realisierung der Benutzerinteraktionen zuständig. Sie kennt das Modell, dessen Daten sie präsentiert, ist aber nicht für die Verarbeitung dieser Daten zuständig. Des Weiteren ist sie von der Steuerung unabhängig.
- Controller - Die Steuerung verwaltet die Präsentation und das Modell. Sie wird über Benutzerinteraktionen informiert, wertet diese aus und nimmt daraufhin Anpassungen an der Präsentation sowie Änderungen an den Daten im Modell vor.
Der allgemeine Ablauf ist einfach und klar verständlich. Über einen vom Shopsystem definierten Einstieg wird die Zahlungsverarbeitung bei collana pay gestartet und für den Kunden ein dazu gehörendes Frontend ausgeliefert.
Besonderer Augenmerk soll hierbei auf zwei Aspekte gelegt werden:
- Der Aufruf
\FlenoCollanaPay\Api::CreateTransaction()
, der die einzige synchrone Kommunikation mit collana pay beinhaltet. - Die Definition der Callback-URL für collana pay. Über diese wird die asynchrone Kommunikation abgewickelt.
Beides werden wir im Folgenden detaillierter betrachten.
1. CreateTransaction()
namespace FlenoCollanaPay;
use GuzzleHttp\Client;
class Api
{
/**
*
* @param array $body
*/
public static function CreateTransaction(array $body)
{
$client = new Client();
$response = $client->post(sprintf('%sapi/v1/transactions', self::getCollanaPayEndpoint()), [
'body' => json_encode(self::prepareBody($body), JSON_UNESCAPED_SLASHES),
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'ApiKey' => self::getApiKey()
]
]);
if($response->getStatusCode() !== 202)
{
throw new \Exception('invalid status code');
}
return json_decode($resp->getBody(), true);
}
/**
*
* @param array $body
*/
private static prepareBody(array $body)
{
return array_merge([
'callback' => [
'uri' => [
'type' => 'HttpJson',
'uri' => sprintf('%sapi/collanapay', \FlenoCollanaPay\Api::getBaseUrl())
]
]
], $body);
}
}
Der Aufbau dieser Methode ist denkbar einfach und auf das wesentliche reduziert. Nur angedeutet ist die Fehlerbehandlung, die auf alle HTTP-Statuscodes außer 202
mit einer Exception reagiert. In der Realität ist eine deutlich detailliertere Fehlerbehandlung zu empfehlen.
Zu empfehlen ist vor allem eine Unterscheidung des HTTP-Statuscode nach 200
, 300
, 400
und 500
, um auf spezifische mögliche Fehlerquellen reagieren zu können. Gerade im Fehlerfall ist auch ein Logging empfehlenswert. Da dies aber generelle Anforderungen an die Entwicklung einer API-Schnittstelle sind, die nicht spezifisch für collana pay gelten, genügt uns in diesem Fall der Hinweis darauf.
2. Callback-URL
Die Callback-URL, die im body mitgegeben wird, bildet den zentralen Kommunikationspunkt zwischen dem Backend und collana pay. In dieser Implementierung lautet die Callback-URL example.com/api/collanapay
. An diese wird collana pay alle asynchronen Antworten senden.
Die Implementierung soll also zwei Aufgaben übernehmen:
- Die HTTP-Requests von collana pay entgegennehmen und verarbeiten
- Als Reaktion auf eine asychron empfangene Anfrage den nächsten Teilschritt, sofern notwendig, anstoßen.
Zu 2. hat dieses Verfahren den Vorteil, dass keine Verzögerungen bei der Verarbeitung auftreten, da die Reaktion des Backends ohne Umweg direkt erfolgt:
Aus Sicht des Backends ist die Callback-URL also passiv, denn sie reagiert lediglich auf die asynchronen Antworten von collana pay. Technisch betrachtet ist eine ansynchrone Antwort im HTTP-Protokoll natürlich so nicht vorgesehen, es handelt sich vielmehr um einen von collana pay initialisierten POST
-Request, der anwendungsseitig von collana pay ausgeführt wird, jedoch zuvor vom Backend ausgelöst wurde.
Wie eine solche Callback-URL implementiert sein kann, wird im folgenden Codebeispiel deutlich:
class Shopware_Controllers_Api_CollanaPay extends \Shopware_Controllers_Api_Rest
{
/**
* POST-request an /api/collanapay
*/
public function postAction()
{
$body = json_decode($this->request->getRawBody(), true);
$transactionId = $body['transactionId'];
$repository = $this->container->get('models')->getRepository(\FlenoCollanaPay\Models\Payment\Payment::class);
$payment = $repository->findOneBy([
'transactionId' => $transactionId
]);
$body['StatusDetails'] = explode('; ', $body['StatusDetails']);
switch ($body['EndpointType'])
{
case 'Transactions':
if($body['StatusDetails'][0] == 'CreateInteraction')
$this->processCreateInteractionResult($payment, $body);
break;
case 'Prepares':
if($body['StatusDetails'][0] == 'PrepareInteraction')
$this->processPrepareInteractionResult($payment, $body);
break;
case 'Reservations':
if($body['StatusDetails'][0] == 'ReservationInteraction')
$this->processReservationInteractionResult($payment, $body);
break;
}
$this->response->setStatusCode(200);
$this->response->setBody(json_encode([
'success' => true
]));
$this->response->send();
}
}
Hinweis: Anstelle der Verwendung des StatusDetails-Knotens, bietet sich die Verwendung des Knotens EndpointType
des Callbacks an.
Dieser Controller (MVC, s.o.) reagiert auf POST-Requests auf der URL /api/collanapay
, wie bereits beschrieben. Er erfüllt die beiden o.g. Aufgaben, indem er den HTTP-Request-Body parst, und im ersten Schritt mithilfe der transactionId
die Verbindung zum zu behandelnden Zahlungsvorgang herstellt. Auch hier wird aus Gründen der Übersichtlichkeit auf Fehlerbehandlung, Logging etc. verzichtet.
In der Folgenden switch...case
-Anweisung wird nun auf die verschiedenen möglichen Antworten von collana pay eingegangen. Betrachten wir direkt den ersten Fall, handelt es sich hierbei um die Verarbeitung der asnychronen Antwort auf das erstellen der Transaktion. Synchron erhielt das Backend bereits die transactionId
zurück, nun meldet collana pay auch asynchron das Erstellen der Transaktion. Die dann aufgerufene Methode processCreateInteractionResult()
kann nun wie folgt implementiert werden:
class Shopware_Controllers_Api_CollanaPay extends \Shopware_Controllers_Api_Rest
{
/**
* @var \FlenoCollanaPay\Models\Payment\Payment $payment
* @var array $body
*/
private function processCreateInteractionResult(\FlenoCollanaPay\Models\Payment\Payment $payment, $array $body)
{
\FlenoCollanaPay\Api::PrepareTransaction($body['TransactionId'], [
'...' // payload für /transactions/{id}/prepare
]);
$payment->status = 'created';
$payment->action = '';
$this->container->get('models')->flush();
}
}
Hier wird deutlich, dass das Backend beide Aufgaben erfüllt: Es bildet den Zustand der Zahlung lokal persistent ab und initiiert den nächsten Schritt. Die Implementierung der Methode ::PrepareTransaction()
ist sehr ähnlich zur bereits bekannten Methode ::CreateTransaction
(s.o.) und kann etwa so aussehen:
namespace FlenoCollanaPay;
use GuzzleHttp\Client;
class Api
{
/**
*
* @param array $body
*/
public static function PrepareTransaction($transactionId, array $body)
{
$client = new Client();
$response = $client->post(sprintf('%sapi/v1/transactions/%s/prepares', self::getCollanaPayEndpoint(), $transactionId), [
'body' => json_encode(self::prepareBody($body), JSON_UNESCAPED_SLASHES),
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'ApiKey' => self::getApiKey()
]
]);
if($response->getStatusCode() !== 202)
{
throw new \Exception('invalid status code');
}
return json_decode($resp->getBody(), true);
}
}
Zu beachten ist, dass dieser Codeauschnitt Bezug nimmt auf die weiter vorne gezeigte Implementierung dieser Klasse. Dies ist besonders wichtig im Hinblick auf die Methode prepareBody
, die dafür sorgt, dass jeder Request an die collana pay-API die bekannte Callback-URL erhält.
Zusammenfassend ist bis hierher also folgendes geschehen:
- Die asynchrone Antwort auf das Erstellen der Transaction wurde empfangen.
- Die Information daraus wurde als persistenter Zustand der Zahlung, identifiziert durch ihre Transaktions-ID, im Shop hinterlegt.
- Der nächste Schritt der Verarbeitung wurde angestoßen, das Backend erwartet nun also wieder auf der Callback-URL die nächste asynchrone Antwort von collana pay.
Betrachten wir nun die Implementierung für den nächsten Schritt, die Antwort von collana pay auf den /prepares
-Aufruf:
class Shopware_Controllers_Api_CollanaPay extends \Shopware_Controllers_Api_Rest
{
/**
* @var \FlenoCollanaPay\Models\Payment\Payment $payment
* @var array $body
*/
private function processPrepareInteractionResult(\FlenoCollanaPay\Models\Payment\Payment $payment, $array $body)
{
$payment->status = 'prepare';
$payment->action = 'iframe';
$payment->redirectUrl = $body['EmbedmentUri'];
$this->container->get('models')->flush();
}
}
Wie aus dem Ablaufdiagramm bekannt, liefert collana pay als Reaktion auf /prepares
eine URL zurück, die das Frontend in einem Iframe anzeigen soll. Diese Information wird in den lokalen Zustand geschrieben und gelangt unmittelbar darauf via Longpolling zum Frontend. Dort wird der Kunde das Formular ausfüllen, dies wird einen weiteren asynchronen Aufruf an die Callback-URL auslösen.
Da wir uns aber noch immer im /prepares
-Schritt befinden, wird dieser Aufruf wieder in dieser Methode landen, da der EndpointType
der Anfrage weiterhin Prepares
lautet. Daher muss die Methode erweitert werden, um beide Fälle abdecken zu können:
class Shopware_Controllers_Api_CollanaPay extends \Shopware_Controllers_Api_Rest
{
/**
* @var \FlenoCollanaPay\Models\Payment\Payment $payment
* @var array $body
*/
private function processPrepareInteractionResult(\FlenoCollanaPay\Models\Payment\Payment $payment, $array $body)
{
switch($body['StatusDetails'][1])
{
case 'Pending':
$payment->status = 'prepare.pending';
$payment->action = 'iframe';
$payment->redirectUrl = $body['EmbedmentUri'];
break;
case 'Successful':
\FlenoCollanaPay\Api::ReserveTransaction($body['TransactionId'], [
'...' // payload für /transactions/{id}/reserve
]);
$payment->status = 'prepare.done';
$payment->action = '';
$payment->redirectUrl = '';
break;
}
$this->container->get('models')->flush();
}
}
Die Implementierung von ::ReserveTransaction()
folgt dem bekannten Muster, daher gehen wir hier nun nicht näher drauf ein.
Nun folgt das Verarbeiten der asynchronen Antwort darauf, ebenfalls nach bekannten Muster:
class Shopware_Controllers_Api_CollanaPay extends \Shopware_Controllers_Api_Rest
{
/**
* @var \FlenoCollanaPay\Models\Payment\Payment $payment
* @var array $body
*/
private function processReserveInteractionResult(\FlenoCollanaPay\Models\Payment\Payment $payment, $array $body)
{
switch($body['StatusDetails'][1])
{
case 'Pending':
$payment->status = 'reserve.pending';
$payment->action = 'redirect';
$payment->redirectUrl = $body['RedirectUri'];
break;
case 'Successful':
$payment->action = 'success';
$payment->status = 'reserve.done';
$payment->redirectUrl = null;
break;
}
$this->container->get('models')->flush();
}
}
Hinweis: Auch hier kann anstelle der Verwendung des StatusDetails-Knotens, der Knoten Status
des Callbacks verwendet werden.
Diese Implementierung zeigt auf, dass jeder Teilschritt wieder auf denselben Controller zurückgeführt wird. Die Ablauf-Logik wird also von der switch-case
-Anweisung gesteuert, die auf den EndpointType
der asynchronen Antwort reagiert. Je nach Teilschritt wird der aktuelle Status an das payment
-Model übergeben, welches damit zu jedem Zeitpunkt über den aktuellen Status informiert ist.
Frontend
Wie bereits erwähnt, setzt die hier beschriebene Implementierung auf Longpolling auf, um den Datenaustausch zwischen Webbrowser und Webserver bereitzustellen. Auf die Überlegenheit von Longpolling gegenüber traditionellem Polling wird im folgenden Abschnitt noch näher eingegangen. Aus rein technischer Sicht wäre die Verwendung von Websockets noch besser, allerdings verzichten wir in diesem Fall darauf. Es ist im E-Commerce-Umfeld notwendig, eine möglichst breite Kompatibilität zu gewährleisten, um möglichst viele Kunden bedienen zu können. Bei modernen Technologien wie Websockets ist dies nicht immer gegeben, wohingegen Longpolling keine eigene Technologie, sondern lediglich ein Entwurfsmuster basierend auf einfachen, etablierten Technologien beschreibt. Auch beim Einsatz von Websockets müssen für ältere Systeme Fallback-Lösungen angeboten werden, die wiederrum auf den im Folgenden beschriebenen Verfahren basieren werden. Auch die Verwendung von Bibliotheken wie sockets.io
wird hier nicht betrachtet, da diese auch nur das hier Beschriebene umsetzen. Zudem sind Bibliotheken von Haus aus generalisiert, was zwei Konsequenzen mit sich bringt, die wir hier nicht annehmen wollen:
- Eine auf die eigenen Systeme abgestimmte Implementierung ist in vielen Fällen performanter.
- Die Kapselung sorgt dafür, dass Entwickler die tieferen Funktionsweisen nicht kennen (müssen), was sich aber bei auftretenden Problemen oftmals als Nachteil erweist.
Gerade der zweite Punkt widerspricht dem Gedanken dieses Leitfadens, ein tiefgehendes Verständnis für die Funktionsweise von collana pay und seine Integration zu schaffen. Zudem geht dieser Leitfaden von der Prämisse aus, das bestehende Onlineshop-Systeme an collana pay angebunden werden sollen. Viele etablierte Systeme basieren oft auf älteren, gut entwickelten und verbreiteten Technologien.
Beim traditionellen Polling sendet ein Client in regelmäßigen Abständen zum Nachrichtenabruf einen Request an den Server, der vom Server unmittelbar beantwortet wird: entweder mit einer Nachricht, falls eine solche vorliegt, oder andernfalls mit einer leeren Antwort.
Beim Longpolling sendet ein Client zum Nachrichtenabruf ebenfalls einen Request an den Server. Falls eine Kurznachricht für den Client vorliegt, beantwortet der Server den Request unmittelbar, also wie beim traditionellen Polling. Falls für den Client jedoch keine Kurznachricht vorliegt, wartet der Server mit einer Antwort und hält damit die Verbindung offen, bis eine Nachricht für den Client vorliegt oder eine gewisse Zeitspanne verstrichen ist (Timeout). Sobald der Client eine Antwort erhält, sendet der Client einen weiteren Request an den Server.
Der Server übernimmt also in Teilen die Arbeit des Clients, indem er eine Schleife implementiert und die regelmäßige Aktualisierung der Anfrage übernimmt. Der große Vorteil hierbei ist, dass der Overhead ständiger TCP-Verbindungen (Aufbau, Abbau, Datenübertratung) reduziert wird.
Zudem lässt sich mit Longpolling nahezu das Gefühl von Echtzeit-Reaktionen umsetzen, während beim traditionellen Polling unter Berücksichtigung der Ressourcen von Client & Server fast immer eine spürbare Verzögerung gegeben ist.
Beispiel
Um das Prinzip zu verdeutlichen, im folgenden eine minimale Implementierung von Longpolling.
Client
Die hier gezeigte minimale Implementierung von Longpolling basiert auf jQuery. Von der Route /messages
werden neue Nachrichten abgefragt. Wenn die Antwort leer ist, wird dies ignoriert. Wenn es zu einem Fehler kommt (z.B. Timeout), wird dies auch ignoriert. Mithilfe von always()
wird sichergestellt, dass in jedem Fall nach Antwort des Servers, egal welcher Art sie ist, wieder ein neuer Request gestartet wird. Somit besteht quasi durchgängig eine Verbindung zum Server.
document.asyncReady(function()
{
function longpolling() {
jQuery.ajax('/messages', {
method: 'GET',
dataType: 'json'
}).success(function(response) {
if(!response.messages.length)
return;
// verarbeite response.messages
}).always(function() {
longpolling();
});
};
longpolling();
});
In der Realität werden oftmals zwei bedeutende Erweiterungen definiert:
- Der Aufruf von
longpolling()
im.always()
-Callback wird oftmals in einemwindow.setTimeout()
gekapselt, wodurch die Resourcenlast des Clients gesteuert werden kann. - Der Fehlerfall wird mit dem Callback
.fail()
überwacht, zuviele Fehler in kurzer Zeit sollten dann den wiederholten Aufruf unterbrechen oder zumindest verzögern (z.B. ein verlängertes timeout in 1.).
Server (PHP / Laravel)
Die serverseitige Implementierung von Longpolling, basierend auf dem PHP-Framework Laravel. Sobald ein neuer Datensatz in der SQL-Tabelle messages
eingetragen wird, wird dieser vom Server zurückgegeben. Solange kein Eintrag vorliegt, wird der Server regelmäßig die Datenbank abfragen.
Es wird ein Timeout definiert (hier: 30 Sekunden), nachdem die Verbindung beendet wird. Dies wird gemacht, um dem Timeout durch tiefere Schichten, etwa das Betriebssystem, zuvorzukommen.
Route::get('messages', function ()
{
$timeout = 30;
$start = time();
do
{
$messages = DB::select('SELECT * FROM messages WHERE unread = 1');
if(!empty($messages))
{
DB::update('UPDATE messages SET unread = 0 WHERE unread = 1');
return response()->json([
'messages' => $messages
]);
}
} while($start + $timeout > time());
return response()->json([
'messages' => []
]);
});
Auch diese Implementierung dient der Veranschaulichung des Prinzips und müsste in der Realität erweitert werden. Unabhängig von der sehr naiven SQL-Verwendung werden vor allem folgende Erweiterungen oftmals vorgenommen:
- Innerhalb der Schleife wird oftmals ein
sleep()
definiert, mit dem die Serverlast reduziert bzw. fein gesteuert werden kann. - In thread-fähigen Sprachen wird die Schleife oftmals in einen eigenen Thread verlagert, um Flaschenhälse in I/O-Streams zu vermeiden.
Anwenden des Verfahrens
Nachdem nun die Prinzipien des Verfahrens klar sind, nutzen wir dies für die eigentliche Implementierung von collana pay.
Zu einem bestimmten Zeitpunkt im Ablauf des Shop-Checkouts wurde die Zahlung via collana pay initiiert, dies wurde im vorherigen Abschnitt "Backend" beschrieben.
Wir setzen ein, nachdem die Verarbeitung der Zahlung durch collana pay initiiert wurde, d.h. eine transactionId
vorliegt. Der Nutzer wurde auf eine Seite innerhalb des Shopsystems geleitet, auf der nun die Verarbeitung durch collana pay durchgeführt wird.
Der Client übernimmt nun also zwei Aufgaben:
- Er dient als Auslöser für den Server, die jeweils nächsten Schritte in der Verarbeitung durchzuführen
- Er informiert den Kunden über den aktuellen Stand der Zahlungsverarbeitung
Wir verwenden also das o.g. Beispiel und passen es ein wenig an:
<div id="collanapay">
<p id="status">Ihre Zahlung wird verarbeitet, bitte warten</p>
</div>
<script src="longpolling.js"></script>
/**
* führt das longpolling durch
*/
var status = null;
function longpolling()
{
jQuery.ajax('/collanapay/status', {
method: 'GET',
dataType: 'json',
data: {
transactionId: transactionId,
status: status
}
}).success(function(response) {
processResponse(response);
}).always(function() {
longpolling();
});
};
/**
* verarbeitet die Antwort des Servers
* @param response
*/
function processResponse(response) {
if(!response.update)
return;
status = response.payload.status.code;
processStatus(response.payload.status);
processAction(response.payload.action);
};
/**
* aktualisiert die Statusmeldung für den Nutzer
* @param status
*/
function processStatus(status) {};
/**
* führt Aktionen im Client aus, die vom Server angefordert werden
* @param action
*/
function processAction(action) {};
document.asyncReady(function()
{
longpolling();
});
In dieser einfachen Implementierung fallen einige Unterschiede zum vorherigen Beispielcode auf.
Zunächst wird der aktuelle Zustand in der Variable status
hinterlegt, welcher zu Beginn noch nicht definiert ist. Diese Information wird dem Server beim Aufbau der Verbindung mitgeteilt, so dass der Server Kenntnis darüber hat, welchen Informationsstand der Client hat.
Die Implementierung von longpolling()
delegiert die Verarbeitung an eine eigene Methode. Neben der Übersichtlichkeit hat dies weitere Vorteile. Zum einen lässt dies Raum für ein - hier nicht implementiertes - Error-Handling, indem etwa der Aufruf von processStatus
in einen try...catch
-Block verpackt wird. Viel wichtiger jedoch ist die Trennung von Prozess und Übertragung, d.h. die bereits erwähnte Implementierung alternativer Kanäle wie Websockets wird auf dieser Seite deutlich vereinfacht.
Folgen wir nur diesem Beispiel, müssen wir davon ausgehen dass der Server auf der Route /collanapay/status
mit einer Nachricht antwortet, die aus zumindest zwei Attributen besteht:
-
status
- Ein Objekt, welches den aktuellen Zustand alscode
und in Textform inmessage
übermittelt. -
action
- Ein Objekt, welches den Client zu weiteren Aktivitäten anregen kann.
Bevor wir die Implementierung der Methoden betrachten, die diese beiden Attribute behandeln, schauen wir uns zunächst die Implementierung auf der Seite des Servers an. Im ersten Beispiel zeigten wir Code auf Basis von Laravel, um schnell & einfach das Prinzip aufzuzeigen.
Ab sofort verwenden wir zur Veranschaulichung (gekürzten) Code aus der Implementierung für das Shopsystem Shopware. Dieser Code wird in der bereits bekannten Klasse Shopware_Controllers_Frontend_CollanaPay
verwendet:
class Shopware_Controllers_Frontend_CollanaPay extends \Shopware_Controllers_Frontend_Payment
{
private $timeout = 30;
public function statusAction()
{
$transactionId = $this->request->getParam('transactionId');
if(!$transactionId)
{
return $this->response->setHttpResponseCode(400)->send();
}
try
{
$repository = $this->container->get('models')->getRepository(\FlenoCollanaPay\Models\Payment\Payment::class);
$payment = $repository->findOneBy($condition, [
'transactionId' => $transactionId
]);
if(!$payment)
{
return $this->response->setHttpResponseCode(404)->send();
}
$start = time();
$update = false;
$payload = null;
while($start + $this->timeout > time())
{
$queryBuilder = $this->createQueryBuilder('payment')->setCacheable(false);
$queryBuilder->where('payment.transactionId = ?1');
$queryBuilder->setParameter(1, $transactionId)
if($status = $this->request->getParam('status'))
{
$queryBuilder->where('payment.status != ?2');
$queryBuilder->setParameter(2, $status);
}
$payment = $queryBuilder->getQuery()->getOneOrNullResult();
if($payment)
{
$payload = [
'status' => [
'code' => $payment->status,
'message' => $payment->status
],
'action' => [
'code' => $payment->action,
'payload' => $payment->redirectUrl
]
];
$update = true;
break;
}
}
return $this->response->setBody(json_encode([
'update' => $update,
'payload' => $payload
]));
}
catch(\Exception $e)
{
return $this->response->setHttpResponseCode(500)->setBody(json_encode([
'message' => $e->getMessage()
]));
}
}
}
In diesem Code wird zunächst die transactionId
geprüft, eine ungültige ID löst einen HTTP-Fehler 400
aus. Aus Gründen der Übersichtlichkeit ist die Prüfung hier lediglich rudimentär implementiert, für den Produktivbetrieb ist eine weitere formale Prüfung zu empfehlen.
Ist kein Statuscode übergeben, antwortet der Server mit dem aktuellen Zustand der Zahlung. Andernfalls antwortet der Server, sobald ein Zustand vorliegt, der nicht dem entspricht, welcher dem Client bekannt ist. Hat der Client also Kenntnis über den Zustand create.done
, wird der Server antworten, sobald der Zustand nicht mehr create.done
ist.
Wie zu sehen ist, antwortet der Server also mit dem aktuellen Zustand der Zahlungsverarbeitung in einer Form, die vom Frontend verstanden werden kann. Die konkreten Daten wurden bereits im vorherigen Kapitel vom Controller, der die Callback-URL von collana pay behandelt, bereitgestellt. Auch hier dient die transactionId
als das verbindende Element.
Da nun bekannt ist welche Daten übermittelt werden, wenden wir uns nun wieder dem Frontend und der Verarbeitung der Daten zu:
/**
* aktualisiert die Statusmeldung für den Nutzer
* @param status
*/
function processStatus(status) {
var $p = $('p#status');
$p.html(status.message);
};
Die Implementierung von processStatus()
ist in diesem Beispiel denkbar simpel und eigentlich selbsterklärend. Der aktuelle Zustand der Zahlungsverarbeitung wird dem Kunden in einfacher Textform dargestellt.
Etwas aufwendiger ist die Verarbeitung der action
, wie im folgenden Beispiel zu sehen ist:
/**
* führt Aktionen im Client aus, die vom Server angefordert werden
* @param action
*/
function processAction(action) {
switch(action.code) {
case 'iframe':
document.location.href = action.payload;
var $iframe = $('<iframe />');
$iframe.attr('src', action.payload);
$('div#collanapay').append($iframe);
break;
case 'redirect':
document.location.href = action.payload;
break;
}
}
document.asyncReady(function()
{
longpolling();
});
Wie aus dem Ablaufdiagramm bekannt, gibt es zwei Aktionen, die das Frontend ausführen muss. Für beide Aufgaben ist hier der rudimentäre Code dargestellt.
Anhang
Im Folgenden noch einige weitere Informationen, die nicht unmittelbar zur Implementierung gehören, jedoch in manchen Fällen hilfreich sein können.
Skalierbarkeit vorgelagerter Webserver
Oft ist dem Anwendungsserver ein Webserver vorgelagert, z. B. ein Reverse Proxy. Dann wird dieser schnell zu einer kritischen Resource für die Long-Polling-Anwendung. Anhand der häufig verwendeten Lösungen Apache und nginx betrachten wir die möglichen Probleme und Lösungsansätze etwas genauer
Apache
Der Apache HTTP Server bietet verschiedene MPMs (Multi-Processing Modules) an, um Requests zu verarbeiten:
- Beim Prefork MPM werden mehrere Prozesse gestartet, die jeweils einen einzigen Thread besitzen, um jeweils einen einzigen Request zu bearbeiten.
- Beim Worker MPM dagegen verwendet ein Prozess mehrere Threads, um pro Thread wiederum jeweils einen Request zu bearbeiten.
Bei beiden MPMs ist also die Anzahl gleichzeitiger Long-Polling-Requests begrenzt durch die Anzahl der verfügbaren Threads. Deren Obergrenze liegt standardmäßig bei 256 bzw. 300. Für deutlich mehr gleichzeitige Long-Polling-Requests (z. B. für 10.000 gleichzeitig aktive Requests) sind diese MPMs daher weniger geeignet. Seit Version 2.4 bietet der Apache HTTP Server daher ein Event MPM an, das besser skalieren soll als die beiden vorgenannten MPMs.
nginx
Nginx setzt dagegen grundsätzlich auf eine Event-getriebene Verarbeitung von Requests, wodurch die Behandlung 10.000 gleichzeitig aktiver Requests durch einen einzigen Thread ermöglicht wird. Das unterschiedliche Verhalten von Nginx zu der Prefork/Worker MPM des Apache HTTP Servers lässt sich bereits bei ca. 400 gleichzeitig aktiven Long-Polling-Requests beobachten: 400 Anfragen überschreiten die Anzahl verfügbarer Threads in der Standard-Konfiguration des Apache HTTP Servers, so dass einige Requests stark verzögert bearbeitet oder sogar zurückgewiesen werden, während der standardmäßig einzige Thread in Nginx alle 400 Long-Polling-Requests direkt an den Anwendungsserver weitervermitteln kann.
Schlussfolgerung
Für Long Polling bei hohen Nutzerzahlen ist also eine Event-getriebene Verarbeitung durch den Webserver empfehlenswert. Die Entscheidung für einen Verarbeitungsmechanismus ist dabei bereits zur Setup-Zeit des Webservers zu treffen: Beim Apache HTTP Server ist das MPM bereits vor der Kompilierung des Apache HTTP Server zu wählen. Bei Nginx bedeutet die Installation von Nginx bereits eine Entscheidung für Event-getriebene Verarbeitung.
Longpolling
Fehlerbehandlung, clientseitig
Die zuvor beschriebene Implementierung von Longpolling verzichtet auf eine clientseitige Fehlerbehandlung. Bei der clientseitigen Implementierung liegt der Fokus vor allem auf dem schonenden Umgang mit Serverressourcen. Dies liegt darin begründet, dass bei hinreichend großer Zahl von aktiven Clients und einer anfälligen Implementierung der Server schnell mit einer unüberschaubaren Anzahl von Requests belastet werden kann. Gängige Strategien zielen also darauf ab, unnötige Requests zu minimieren. Antwortet der Server mit einer Fehlermeldung, reagiert eine primitive Implementierung mit einem sofortigen Neuversuch. Ist das Problem systemisch, wird der Server die Fehlermeldung ständig erneut generieren und somit unter Dauerbelastung des Clients liegen. Ziel soll daher sein, diesen Fall zu erkennen und entsprechend zu reagieren.
Eine mögliche Implementierung kann so aufgebaut werden:
document.asyncReady(function()
{
var longpollingOptions = {
stats: {
success: 0,
fail: 0
},
sleep: 0,
onFail: function() {
if(prompt('Es gibt Probleme. Möchten Sie dennoch warten?')) {
longpollingOptions.stats.success = 0;
longpollingOptions.stats.fail = 0;
longpollingOptions.sleep = 100;
}
}
};
function longpolling() {
jQuery.ajax('/messages', {
method: 'GET',
dataType: 'json'
}).success(function(response) {
longpollingOptions.stats.success++;
longpollingOptions.sleep = 0;
if(!response.messages.length)
return;
// verarbeite response.messages
}).fail(function() {
longpollingOptions.stats.fail++;
longpollingOptions.sleep += 10;
}).always(function() {
if(longpollingOptions.stats.success < longpollingOptions.stats.fail) {
return longpolling.onFail();
}
window.setTimeout(longpolling, longpollingOptions.sleep);
});
};
longpolling();
});
Diese Implementierung verfolgt 2 Strategien zugleich:
Zum einen wird im Fehlerfall die Zeit bis zum Neuversuch erhöht (longpollingOptions.sleep
). Somit wird ein Client automatisch immer ressourcen-sparsamer. Dies geht natürlich zulasten des Komforts, sollte also kommuniziert werden.
Zum anderen führt diese Implementierung quasi Buch über erfolgreiche und fehlerhafte Verbindungen. Nimmt die Zahl der Fehler zu und überschreitet eine gewisse Grenze (denkbar wären neben dem gezeigten Vergleich auch absoluten Zahlen oder prozentuale Anteile aller Request), kann die Longpolling-Anwendung abgebrochen werden.
Abschließende Hinweise
Dieser Leitfaden wurde in Kooperation zwischen der collana hive GmbH und der Fleno GmbH verfasst. Er dient als Beispiel und Rahmenwerk einer möglichen Integration in Shopsysteme. Die konkrete Integration ist immer individuell an die Situation und das Shopsystem anzupassen und zu optimieren.
collana hive GmbH
Borselstraße 20
22765 Hamburg
support@collanapay.com
Fleno GmbH
Marie-Curie-Ring 31
24941 Flensburg
kontakt@fleno-gmbh.de