Anleitungen eCommerce Erfahrene PHP Symfony Web Development

Der Leitfaden zur Paypal Integration in PHP

Vor Kurzem hatte ich mich mit der Paypal Integration in PHP für eine eCommerce-Lösung beschäftigt. Dabei begegneten mir leider ein paar Stolpersteinen. Mir fiel auch auf, dass vermehrt Probleme bei der Integration von Paypal auch bei anderen vorhanden sind.
Daher habe ich einen Leitfaden für die Paypal Integration in PHP erstellt, damit Du schneller mit der Entwicklung bist.

Möglicherweise kommen Dir ein paar Probleme banal vor, jedoch finde ich es trotzdem wichtig sie zu schildern. Es ist ja auch möglich, dass Du in die gleichen Probleme läufst.

Was Du für eine Paypal Integration in PHP brauchst?

Ich gehe davon aus, dass der Shop an dem Du arbeitest nicht nur ein Produkt verkauft, sondern verschiedene in unterschiedlichen Kombinationsmöglichkeiten. Falls Du nur ein einziges Produkt verkaufst, dann sollte der Einbau eines Paypal-Buttons die geeignetere Lösung sein.

Selbstverständlich gibt es für sehr viele Shops bereits Modul oder Plugins für die Bezahlung mit Paypal. Der folgende Artikel befasst sich damit, wie Paypal ohne Plugin integriert werden kann.

1.1 Wie Du eine REST-Api App erstellst

Ohne eine Paypal REST-API App kannst Du die Paypal Integration in PHP vergessen.

Daher erkläre ich Dir in den einzelnen Schritten, was Du dafür tun musst.

Geschäftskonto erstellen

Um eine Paypal REST-API App erstellen zu können, sind zwei Dinge notwendig. Zuerst wird ein Paypal Account benötigt. Wenn eines vorhanden ist, solltest Du prüfen, ob es sich um ein privates oder ein geschäftliches Konto handelt. Um Paypal in einen Online-Shop zu integrieren, ist ein Geschäftskonto notwendig.

Nun gibt es zwei Möglichkeiten:

  1. Umwandlung des vorhanden Accounts in ein Geschäftskonto
  2. Erstellung eines neuen Geschäftskontos

Persönlich empfehle ich Variante zwei. Dann sind private und geschäftliche Zahlungen voneinander getrennt.

Es ist jedoch Dir überlassen.
Entscheide selbst.

Wie du eine Paypal REST-API App erstellst

Im nächsten Schritt zur Paypal Integration in PHP ist die Erstellung einer sog. Web App. Auf dem Paypal Developer Portal notwendig. Logge Dich mit Deinem Geschäftskonto ein und betreten das Dashboard. Nun kannst Du eine App über den Button „Create App“ erstellen.

Developer Paypal Rest-API App
Developer Paypal Rest-API App

Nach der Erstellung kannst Du für die Sandbox (Testumgebung) und den Orginal-Zugang (Live) die jeweilige ClientID und den Secret finden. Beides ist notwendig um eine gültige Authentifizierung zu erhalten. Bis die Entwicklung abgeschlossen ist, wird alles über die Sandbox getestet. Grundsätzlich sollten alle Entwicklungen und dessen Tests über die Sandbox laufen.

REST-API App Einstellungen anpassen

Bevor du loslegst, solltest Du die nachfolgenden Einstellungen WIRKLICH ZUERST setzen.

Der Grund ist einfach: Paypal braucht oft länger um geänderte Einstellungen wirkungsvoll zu machen.

Viele Entwickler versuchen direkt nach den Änderungen korrekte Ergebnisse zu erzielen.

Und sitzen Stunden lang am Code und versuchen verzweifelt die Ursache von Problemen zu finden.

Auch ich habe diesen Zustand erlebt. Du solltest ganz klar warten, bis deine Änderungen vollständig umgestellt sind. Investiere in dieser Zeit lieber in andere wichtige Dinge und schone deine Nerven.

Die Einstellungen Return URLs sind die vorerst die Wichtigsten. Dabei sollten die URLs für die Rückkehr von Paypal eingestellt werden. Die Einstellungen kannst du gleich für die Sandbox, wie auch für das Live-System machen.

Wenn Du dir nicht sicher bist, wie deine URLs lauten werden, dann mache dir erst einmal darüber deine Gedanken.

In unserem Beispiel wird die gleiche URL verwendet, jedoch mit unterschiedlichen GET-Parametern. Die Erfolgs-URL besitzt den GET-Parameter success=1. Die URL bei Abbrüchen hat den GET-Parameter cancel=1. Der Abschnitt Die Rückkehr von Paypal geht tiefer in die Verarbeitung der Daten ein.

Developer Paypal REST-Api Settings
Developer Paypal REST-Api Settings

Test-Accounts für Deine Sandbox anlegen

Neben der REST-API App sollten noch diverse Test-Accounts angelegt werden. Dazu musst Du in den Bereich Accounts gehe. Erstelle jedoch nicht nur einen Account, sondern verschiedene Typen. Schließlich willst du verschiedene Varianten prüfen, wie bspw. mit Kreditkarte, ohne Kreditkarte, kein Paypal-Guthaben, etc. Mithilfe der verschiedenen Accounts können die Rückmeldungen von Paypal untersucht werden.

„PHP SDK for PayPal RESTful APIs“ einbinden

Um eine vernünftige Paypal Integration in PHP vorzunehmen, sollte die PHP SDK von Paypal verwendet werden. Um die aktuelle Version in Ihr Projekt zu integrieren kann bspw. Composer genutzt werden. In die Datei composer.json im Abschnitt „require“ muss folgende Zeile eingefügt werden:

"require": {
        ...
        "paypal/rest-api-sdk-php" : "dev-master"
    },

Danach kann über die Konsole die Installation über folgenden Befehl ausgeführt werden: composer update. Generell müsste das ausreichen. Sollten jedoch noch Komponenten für die SDK fehlen, wird Composer die Information weitergeben. Die fehlenden Komponenten müssten dann vorher installiert werden.

Ein wirklich großes Problem, dass bei meiner ersten Implementierung geschah, war die Verwendung einer alten Version der Paypal SDK. Anstelle der Versionierung dev-master wurde eine ältere Version in Composer angegeben und damit installiert. Die Verschlüsselungstechnik hatte sich bis dahin jedoch geändert und es wurde bei jedem Aufruf von Paypal ein sog. Hand-Shake-Error geworfen. Der Fehler klingt zwar sehr banal, es dauerte jedoch Stunden um den Fehler zu ermitteln.

Mit der Verwendung der Versionierung dev-master sollten Sie jedoch bei Updates mittels Composer darauf achten, dass auch die SDK ein Update erhalten könnte. Die bisherige Funktionalität Ihrer Paypal Integration sollte daraufhin nochmals überprüft werden.

2. Die ersten Schritte im Code für die Paypal Integration in PHP

Je nach Implementierung Deines Online-Shops, kann die Einbindung der API-Komponenten anders ablaufen.
Es spielt auch eine Rolle ob bspw. ein PHP-Framework genutzt wird.
Wenn Du eines nutzt, muss der Code natürlich daran angepasst werden. Zu deiner Info: Das Beispiel nutzt das Framework Symfony und ist mit Version 2.8 umgesetzt.

Solltest du ein anderes Framework oder gar eine höhere Symfony Version nutzen, musst du den Code natürlich anpassen. Das ist Dir wahrscheinlich schon klar.

Damit Du Paypal nun als Bezahlmethode nutzen kannst, muss Paypal auf einen möglichen Bezahlung vorbereitet werden. Die Softwarearchitektur hängt natürlich stark von Deiner eCommerce Lösung ab.
Für das Beispiel wurde extra eine weitere Klasse erzeugt, welche für die Ablauflogik von Paypal zuständig ist.

Grundfunktionen der eigenen Klasse

Die Grundfunktionen der Klasse basieren auf zwei Methoden, dem Konstruktor und der Methode mit dem Namen fnSetEnvironmentDetails:

namespace MyShopBundle\Controller\Shop\Payment;

use Symfony\Bundle\FrameworkBundle\Controller\Controller,
    Symfony\Component\HttpFoundation\Session\Session;

// Paypal requirements
use PayPal\Rest\ApiContext;
use PayPal\Api\OpenIdSession;
use PayPal\Auth\OAuthTokenCredential;
use PayPal\Api\Address;
use PayPal\Api\PayerInfo;
use PayPal\Api\Payer;
use PayPal\Api\Item;
use PayPal\Api\ItemList;
use PayPal\Api\Details;
use PayPal\Api\Amount;
use PayPal\Api\Transaction;
use PayPal\Api\Payment;
use PayPal\Api\RedirectUrls;
use PayPal\Api\PaymentExecution;

class PaypalPayment extends Controller {
protected $container;
protected $request;
protected $totalsCalculator;
protected $paymentInformation;	

protected $prodClientId = "";
protected $prodSecret   = "";
protected $devClientId 	= "";
protected $devSecret    = "";
protected $clientId	= "";
protected $secret       = "";

public function __construct($container, $devMode = false, $clientId = false, $secret = false, $devClient = false, $devSecret = false) {
   $this->container    	   = $container;
   $this->get('request');
   $this->get('total_calculator');

   if ($clientId === false AND $this->container->hasParameter('PaypalClientId') === false )
	throw new \Exception('No existing Parameter "PaypalClientId"! Please add this parameter.');
   else
	$this->prodClientId = $this->container->getParameter('PaypalClientId');

   if ($secret === false AND $this->container->hasParameter('PaypalSecret') === false )
	throw new \Exception('No existing Parameter "PaypalSecret"! Please add this parameter.');
   else
	$this->prodSecret   = $this->container->getParameter('PaypalSecret');

   if ($devMode === true) {
	if ($clientId === false AND $this->container->hasParameter('PaypalDevClientId') === false )
	   throw new \Exception('No existing Parameter "PaypalDevClientId"! Please add this parameter.');
	else
	   $this->devClientId  = $this->container->getParameter('PaypalDevClientId');

	if ($clientId === false AND $this->container->hasParameter('PaypalDevSecret') === false )
	   throw new \Exception('No existing Parameter "PaypalDevSecret"! Please add this parameter.');
	else
	   $this->devSecret    = $this->container->getParameter('PaypalDevSecret');
   }

   $this->fnSetEnvironmentDetails($devMode);
}

protected function fnSetEnvironmentDetails($devMode = false) {
   if ($devMode === false ) {
	$this->clientId    = $this->prodClientId;
	$this->secret      = $this->prodSecret;
  }
  else {
        $this->clientId    = $this->devClientId;
	$this->secret      = $this->devSecret;
   }
}

Der Konstrukter erhält das Container-Objekt von Symfony (s. $container). Weitere Argumente sind gesetzt um verschiedene Tests zu machen. Der Container kann verschiedene andere Objekte aus dem System laden um damit zu arbeiten. Mit ihm können auch gesetzte Parameter aus dem System geladen werden.

Daher holt der Container bereits im Konstruktor die Parameter Client-ID und den dazugehörigen Secret. Sind die Parameter nicht gesetzt, werden sog. Exceptions geworfen. Die Prüfung auf Nutzung der Sandbox oder dem Live-Betrieb findet ebenfalls im Konstruktor statt. Der Wert des Arguments $devMode entscheidet es. Ist der Wert false, wird der Live-Betrieb gestartet. Der Wert true lässt die Sandbox aktiv werden, der über die Methode fnSetEnvironmentDetails gesetzt wird.

Ich finde diese Art der Entwicklung wichtig, egal ob die Paypal Integration in PHP statt findet, oder in einer anderen Programmiersprache. Das gilt auch für andere Bezahlmöglichkeiten, wenn diese vom Anbieter angeboten wird.

Der weitere Aufbau der Klasse „PaypalPayment“

Der nächste Schritt, um die Bezahlung mit Paypal zu ermöglichen, befasst sich mit der Vorbereitung von Paypal.

public function fnGetPaymentInformation($devMode = false){
   if (empty($this->paymentInformation))
      $this->paymentInformation = $this->fnBuildPaymentInformation($devMode);

       return $this->paymentInformation;<br>
}
protected function fnBuildPaymentInformation($devMode) {
   $totalValues = $this->totalsCalculator->fnGetTotalValues();
   $paymentInformation['total']         = $totalValues['total'];
   $apiContext		                = $this->fnGetApiContext($devMode);
   $paymentInformation['paypalPayment']	= $this->fnCreatePaypalPayment($apiContext);
    return $paymentInformation;
}
   protected function fnCreatePaypalPayment($apiContext) {
   $payer		= new Payer();
   $payer-&gt;setPaymentMethod("paypal");
   $transaction	= $this->fnBuildTransaction();
   $baseUrl	= $this->request-&gt;getSchemeAndHttpHost();
   $responseUrl = '/your-paypal-payment-response-url/';
   $redirectUrls 	= new RedirectUrls();
   $redirectUrls->setReturnUrl($baseUrl . $responseUrl .'?success=true')
		->setCancelUrl($baseUrl . $responseUrl .'?cancel=true');
   $payment 	= new Payment();
   $payment->setIntent("sale")
	   ->setPayer($payer)
	   ->setRedirectUrls($redirectUrls)
           ->setTransactions(array($transaction));
   $paypalPaymentObj   = $payment-&gt;create($apiContext);
   return $paypalPaymentObj;
}
protected function fnGetApiContext($devMode = false) {
   $oauthCredential  = new OAuthTokenCredential($this->clientId, $this->secret);
   $apiContext 	     = new ApiContext($oauthCredential);
   $apiContext->setConfig(
      array(
         'mode' => ($devMode === false ? 'live' : 'sandbox'),
	 'log.LogLevel' => ($devMode === false ? 'INFO' : 'DEBUG'), //'DEBUG', // PLEASE USE `INFO` LEVEL FOR LOGGING IN LIVE ENVIRONMENTS
      )
    );
    return $apiContext;
}
protected function fnBuildTransaction($orderId = false) {
   $totalValues	= $this->totalsCalculator->fnGetTotalValues();
   $bagItemsForPP	= array();
   $currentSubTotal	= 0.00;
   foreach( $bagItems = $this-&gt;totalsCalculator->fnGetConfiguratedBagItems() as $index => $singleBagItem) {
        $currentSubTotal += ($singleBagItem['price'] - $singleBagItem['discount_amount_of_single_article']) * $singleBagItem['quantity'];
	$bagItemsForPP[$index]	= new Item();
	$bagItemsForPP[$index]->setName($singleBagItem['fullProductName'])
			      ->setCurrency($this->container->hasParameter('currency_normal') ? $this->container->getParameter('currency_normal') : '');

Ich gehe davon aus, dass Du PHP verstehst. Darum werde ich den obigen Code nicht 100% erklären.

Du solltest jedenfalls wissen, dass ein paar Dinge extrem wichtig sind, um die Bezahlung überhaupt möglich zu machen. Auf diese Punkte gehe ich jetzt ein.

Hinweise und mögliche Fehlermeldungen

Der sog. ApiContext aus der REST API wird genutzt um eine valide Verbindung zu Paypal aufzubauen. Das Objekt prüft die ClientID und den Secret. Bei falschen Werten gibt es einen 404-Fehler zurück. Der Fehler taucht jedoch nur auf, wenn die direkte Verbindung aufgebaut wird. Der Verbindungsaufbau erfolgt durch folgende Zeile:
$paypalPaymentObj = $payment->create($apiContext);

Theoretisch ist es nicht notwendig die Artikel einer Bestellung an Paypal weiterzugeben. Im Leitfaden zur Paypal Integration in PHP soll neben den notwendigen Punkten, auch hilfreiche Punkte aufzeigen. Daher habe ich die Übergabe von Artikeln ebenfalls eingebaut. Mithilfe der Übergabe lassen sich schließlich Fehler leichter aufspüren. Sie könnten z.B. beim Einsatz von Rabatten entstehen. Die Übergebenen Werte sendet Paypal übrigens nach der Bezahlung mit. Daher können am Ende von Testbestellungen ebenfalls die Werte geprüft werden. Du solltest auch daran denken, dass du mit den Informationen ggf. leichter Statistiken über Paypal-Bezahlungen machen könntest. Das nur am Rande.

Ich erwähne das, dann es mir selbst so erging, dass der berechnete Warenkorbwert (s. Variable $totalValues) größer war, als der wirkliche Warenkorb-Wert. Die Übergabe der korrekt rabattierten Preise haben nicht geklappt. Da die Werte nicht zueinander gepasst haben, hat Paypal einen 404-Fehler geworfen. Denn Paypal vergleicht schließlich die Werte der Waren und den Gesamtwert des Warenkorbes.

Einbau in ein Template

Der Einbau des Links zur Paypal-Bezahlmaske ist relativ simpel. Über den Aufruf der Funktion fnGetPaymentInformation der Klasse PaypalPayment erfolgt eine Rückgabe der Instanz der Klasse Payment (Paypal REST API). Anschließend übergibst Du den Link über diese Funktion an den View oder Template: $paypalPayment->getApprovalLink();. Anschließend muss die Variable im Template nur noch eingesetzt werden. Der Einbau erfolgt schließlich als Link, ohne Formular.

Die Rückkehr von Paypal

Der nächste Schritt bei der Paypal Integration in PHP ist die Verarbeitung der Daten nach der Rückkehr von Paypal. Wir betrachten nun die Abarbeitung in der sog. Erfolgs-URL (Weiterleitung bei erfolgreicher Bezahlung) und Cancel-URL (Weiterleitung bei Abgebrochener Bezahlung).

Rückgabefunktion nach einer Paypal-Bezahlung

Unserem Beispiel nutzt die gleiche URL für die Rückkehr im Erfolgsfall, sowie im Stornierungsfall. Der Unterschied sind nur unterschiedliche GET-Parametern.
Ich erwähnte bereits, dass es natürlich Ihre Entscheidung ist. Sie sollte wirklich von der Softwarearchitektur Deines Systems abhängen. Ihre Entscheidung über die URLs kann sich natürlich von diesem Beispiel unterscheiden. Der Entwickler orientiert sich hierbei selbstverständlich an der Softwarearchitektur des bestehenden Systems.

Der verantwortliche Controller für die Rückkehr ist wie folgt aufgebaut:

$request        = $this->container->get('request');
$session        = $this->container->get('session');
$translator	= $this->container->get('translator');
	
$paypalSuccess  = ($request->query->has('success') ? true : false);
$paypalCancel   = ($request->query->has('cancel') ? true : false);
$paymentId	= ($request->query->has('paymentId') ? $request->query->get('paymentId') : false);
$token		= ($request->query->has('token') ? $request->query->get('token') : false);
$payerId	= ($request->query->has('PayerID') ? $request->query->get('PayerID') : false);
	
if ($paypalSuccess == true) {
			
   $paypalResponse 	= array(
	'paypalResponse'    => array(
	     'paymentId'	=> $paymentId,
	     'token'	=> $token,
	     'payerId'	=> $payerId,
          ),
    );
			
    return $this->redirect($this->generateUrl('Shop_CheckoutPaymentResponse', $paypalResponse));
			
}
elseif ($paypalCancel == true) {
        $session->getFlashBag()->add('paypal-login-error', $translator->trans($paypalLoginErrorDescription));
        return $this->redirect($this->generateUrl('Shop_CheckoutPayment', array('payment_provider_response' => true)));
}
else {
        $session->getFlashBag()->add('paypal-login-error', $translator->trans('An unexpected error uccurs!'));
	return $this->redirect($this->generateUrl('Shop_CheckoutPayment', array('payment_provider_response' => true)));
}

Da unsere Entscheidung auf einer URL mit unterschiedlichen GET-Parametern basiert, werden diese im Controller abgefragt (s. Zeile 4 – 5). Außerdem werden die Werte paymentId, token und PayerID abgeholt (s. Zeile 6 – 8). Paypal sendet diese Parameter auch über GET an die Erfolgs-URL.

Je nach Fall wird in unserem Beispiel an eine unterschiedliche URL weitergeleitet. Im Erfolgsfall leiten wir auf die allg. Bearbeitungsseite der Bezahlung weiter. Im anderen Fall schickt der Controller den User zur Payment-Seite zurück.

Ist die Bezahlung misslungen, erhält der User entsprechend eine Meldung über die Session (s. Zeile 20 und 25). Im Erfolgsfall werden die Werte von Paypal weitergegeben.

In unserem Fall landet der Kunde wieder auf der Auswahl der Bezahlmöglichkeiten / Payment-Seite. Andere Shops beenden den Checkout hiermit.
Je nachdem, wie Du dich entscheidest, muss der Kunde der Warenkorb wieder füllen, oder nicht. Es hängt selbstverständlich von Deinem Shop ab, wie verfahren soll.

In unserem Fall kann der Kunde eine andere Bezahlmöglichkeit wählen. Mit dieser Entscheidung reduzierst Du ggf. die Abbruchrate. Schließlich gibt es einige Kunden, die nicht nochmals den Warenkorb füllen möchten.

Warum Du einen eigenen Controller für die Response bei der Paypal Integration in PHP nutzen solltest

Du fragst Dich möglicherweise: Warum soll die URL nicht direkt auf den allgemeinen Controller zur Abwicklung der Bezahlung gerichtet werden? Die Entscheidung habe ich getroffen, da ich der Meinung bin, Programmcode so kurz wie möglich zu halten. Zweitens vermeide ich grundsätzlich gleichen Code so gut es geht. Werden diese Regeln missachtet, können Änderungen einen höheren Arbeitsaufwand bedeuten. Kürzere Code-Abschnitte helfen entsprechend den Code schneller zu verstehen. Kommen nun neben Paypal weitere Bezahlmöglichkeiten hinzu, dann summieren sich die Programmzeilen immer weiter.

Bezahldaten über Paypal REST API auslesen

Der Controller für die Abwicklung der Bezahlung prüft die Paypal-Bezahlung wie folgt:

....
class CheckoutPaymentResponse extends Controller {
    public function checkoutPaymentResponseAction(){
    ....
    if ($request->query->has('paypalResponse')) {
	$ppConstructor	= new PaypalPaymentConstructor($this->container);
	$paymentControlResponse 	= $ppConstructor->fnHandlePaypalResponse($paypalResponse = $request->query->get('paypalResponse'));
		....
    }
    ....
}

Zeile 5 bezieht sich auf den Weitergegebenen GET-Parameter paypalResponse. Erkennt der Controller, dass eine Bezahlung über Paypal stattfand, wird ein PaypalPayment-Objekt initialisiert.

Die Methode fnHandlePaypalResponse der Klasse PaypalPayment kümmert sich dann um die Bezahl-Daten.

public function fnHandlePaypalResponse( $paypalResponse, $devMode = false ){
	$apiContext	= $this->fnGetApiContext($devMode);
	$payment 	= Payment::get($paypalResponse['paymentId'], $apiContext);
		
	$execution 	= new PaymentExecution();
	$execution->setPayerId($paypalResponse['payerId']);
		
	$result['payment_response_data']    = $payment->execute($execution, $apiContext);
     $result['payment_information']	= json_encode($result['payment_information']);
        ....
	return $result;
}

Grundsätzlich benötigt man die sog. payerId um das Payment- und Execution-Objekt der Paypal REST APIzu erzeugen. Das erzeugte Execution-Objekt wird als Argument in der Methode execute() des Payment-Objektes übergeben. Dadurch kommen die Daten der Paypal-Bezahlung zurück. In unserem Fall nutzen wir das Array $result um die Daten zu speichern.

Die erhaltenen Daten können aschließend in die Datenbank gespeichert werden. Die Rückgabe von Paypal ist selbst ein Array. Um diese Daten in ein Feld speichern zu können, werden die Daten in das JSON-Format umgewandelt (s. Zeile 9). Dadurch ist es gewährleistet, dass Änderungen der Namen der Schlüssel keinen Einfluss haben. Wenn Du auf die Idee gekommen bistjeden Datensatz in eine eigene Spalte zu speichern, Sollte die Idee aufgekommen sein, jeden Wert des Arrays in eine eigene Spalte speichern, könnten ggf. Problem in der Zukunft entstehen. Wenn Paypal aus irgendeinem Grund die Schlüssel des Arrays ändern sollte, muss entsprechend der Code im Shop angepasst werden.

Der Leitfaden zur Paypal Integration in PHP ist nun vollständig.

Fazit

Der hier vorgestellte Code kann von allen genutzt werden um eine Paypal Integration in PHP durchzuführen. Du solltest jedoch darauf achten den Code entsprechend an Dein System anzupassen.

Solltest Du Fragen haben, Anmerkungen stellen willst oder andere Unklarheiten hast, dann schreib mir einfach.