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 immer wieder ein paar Stolpersteinen. Es fiel mir besonders auf, dass andere Entwickler oft die gleichen Probleme haben.
Daher habe ich mich entschlossen, einen Leitfaden für die Paypal Integration in PHP zu erstellen. Damit sollen andere Entwickler wesentlich schneller werden.

Möglicherweise kommen Dir ein paar Probleme etwas banal vor. Ich finde es jedoch wichtig auch diese zu schildern. Schließlich könntest Du in die gleichen Probleme laufen.

Was Du alles für die Paypal Integration in PHP brauchst?

Ich gehe davon aus, dass der Shop, an dem Du arbeitest, nicht nur ein Produkt verkauft. Wahrscheinlich bietest Du verschiedene Waren an, oder? Falls Du nur ein einziges Produkt verkaufst, dann sollte der Einbau eines Paypal-Buttons die geeignetere Lösung sein.

Wenn Du eine Standard Shop-Software benutzt, gibt es fast immer ein Modul oder Plugin für die Bezahlung mit Paypal.

Ich zeige Dir, wie Paypal ohne ein solches Plugin integriert werden kann.

Wie Du eine REST-API App erstellst

Um eine erfolgreiche Paypal Integration in deinen Shop durchzuführen, brauchst Du eine Paypal REST-API App. Ohne sie kannst Du die Paypal Integration in PHP vergessen.

Ich erkläre Dir daher in 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. Erstens ist ein Paypal Account notwendig. Wenn Du eines hast, 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 für die Paypal Integration in PHP muss eine sog. Web App erstellt werden. Auf dem Paypal Developer Portal kannst Du sie erstellen. 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 durchzuführen. Die Entwicklung der Paypal Applikation erfolgt in der Sandbox.

REST-API App Einstellungen anpassen

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

Der Grund ist einfach: Paypal braucht oft etwas länger um Änderungen an den Einstellungen wirkungsvoll zu machen.

Viele Entwickler versuchen direkt nach eine Änderungen ihr Ziel zu erzielen.

Und sitzen Stunden lang am Code und versuchen verzweifelt die Ursache von Problemen zu finden.
Leider ist die Änderung jedoch nur noch nicht in Paypal verfügbar.

Auch ich habe diesen Zustand erlebt. Du solltest ganz klar etwas warten. Ich warte immer einen ganzen Tag um sicher zu sein. Investiere solange Deine Zeit lieber in andere wichtige Dinge und schone deine Nerven.

Die Einstellung der Werte Return URLs ist vorerst die wichtigste Einstellung. Hier werden die URLs für die Rückkehr von Paypal eingestellt. Die Einstellungen kannst du für die Sandbox und das Live-System gleichzeitig machen.

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

Im Leitfaden wird für beide Fälle 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 nach Paypal ein.

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

Test-Accounts für Deine Sandbox anlegen

Neben der REST-API App sollten außerdem verschiedene Test-Accounts angelegt werden. Dazu musst Du in den Bereich Accounts gehen. Erstelle jedoch nicht nur einen Account. Sondern füge verschiedene Typen hinzu. Du willst ja auch verschiedene Bezahlarten prüfen. Es gibt die Möglichkeit mit Kreditkarte, ohne Kreditkarte, kein Paypal-Guthaben, usw. Mithilfe der verschiedenen Accounts können die Rückmeldungen von Paypal auf die verschiedenen Arten untersucht werden.

„PHP SDK for PayPal RESTful APIs“ einbinden

Für eine vernünftige Paypal Integration in PHP, sollte die PHP SDK von Paypal verwendet werden. Um die aktuelle Version in Dein Projekt zu integrieren, kann bspw. Composer genutzt werden. Folgender Code muss in die Datei composer.json im Abschnitt „require“ eingefügt werden:

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

Danach kann über die Konsole die Installation erfolgen. Über den folgenden Befehl erstellt Composer die Dateien mit allen Abhängigkeiten: composer update. Sollten Komponenten für die Paypal SDK fehlen, wird Composer es Dir melden. 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 nutzte ich eine ältere Version der Paypal SDK. Die Verschlüsselungstechnik hatte sich bis dahin jedoch geändert. Dadurch entstand bei jedem Aufruf von Paypal ein sog. Hand-Shake-Error. Der Fehler klingt zwar sehr banal, es dauerte jedoch einige Stunden um den Fehler zu ermitteln.

Wenn Du nun die Versionierung dev-master nutzen solltest, musst Du noch auf etwas achten. Paypal SDK wird wahrscheinlich erhält sicherlich auch ein Update, wenn Du den Composer-Befehl nutzt. Du solltest bei jedem Update prüfen, ob die bisherige Funktionalität Deiner Paypal Integration noch funktioniert.

Der erste Programmcode bei der Paypal Integration in PHP

Je nach Implementierung Deines Online-Shops, kann die Einbindung der API-Komponenten anders ablaufen.
Es spielt dabei selbstverständlich eine Rolle ob z.B. 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 in Version 2.8 umgesetzt.

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

Damit Du Paypal nun als Bezahlmethode nutzen kannst, muss Paypal auf eine mögliche Bezahlung vorbereitet werden. Die Softwarearchitektur hängt sehr stark von Deiner eCommerce Lösung ab.
Im Leitfaden wirdextra eine neue Klasse erzeugt, welche für die Ablauflogik von Paypal zuständig ist.

Ich erkläre dir später noch warum.

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 Konstruktor erhält das Container-Objekt von Symfony (s. $container). Weitere Argumente sind gesetzt um verschiedene Tests zu machen. Der Container ist vorhanden, da er andere Objekte aus dem System laden kann. Außerdem kann er verschiedene Parameter aus der Parameter-Liste abholen.

Der Container holt bereits im Konstruktor die Parameter Client-ID und den 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 dies. Ist der Wert false, wird der Live-Betrieb gestartet. Der Wert true lässt die Sandbox aktiv werden. Über die Methode fnSetEnvironmentDetails werden die Werte gesetzt.

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 eine Sandbox 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->setPaymentMethod("paypal");
   $transaction	= $this->fnBuildTransaction();
   $baseUrl	= $this->request->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->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->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 unbedingt wissen, dass ein paar Dinge extrem wichtig sind, um die Bezahlung überhaupt möglich machen zu können. Auf diese Punkte gehe ich jetzt ein.

Nützliche Hinweise, Fehlerquellen und 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 gebe ich Dir jedoch noch weitere hilfreiche Tipps.
Das ist einer davon und daher habe ich die Übergabe von Artikeln ebenfalls eingebaut (s. Funktion fnBuildTransaction – foreach Schleife).
Der Grund ist ganz einfach. Mithilfe der Übergabe der Artikel, lassen sich Fehler sehr viel leichter aufspüren. Fehler könnten z.B. beim Einsatz von Rabatten entstehen. Die Übergebenen Werte sendet Paypal übrigens nach der Bezahlung mit. Somit können Testbestellungen anschließend mit den Werten verglichen 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 hatte in einem eigenem Shop System exakt das Problem. Nach einer Nutzung von Promotionen haben die Daten von Paypal und die Daten der Bestellung nicht mehr gepasst. Die Übergabe der korrekt rabattierten Preise hatten damals nicht geklappt. Da die Werte nicht zueinander gepasst haben, hat Paypal einen 404-Fehler geworfen. Denn Paypal vergleicht automatisch die Preise der Waren und den Warenkorbwert.

Durch die Übergabe der Artikel ist die Fehlerbehebung sehr viel einfacher.

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 setzt man den Link über diese Funktion an den View oder das Template: $paypalPayment->getApprovalLink();.
Anschließend muss die Variable im Template nur noch aufgerufen werden. Der Einbau erfolgt als Link, nicht über ein Formular.

Die Rückkehr von Paypal – Der letzte Schritt der Paypal Integration in PHP

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 im Erfolgsfall und Stornierungsfall.

Rückgabefunktion nach einer Paypal-Bezahlung

Das Beispiel nutzt die gleiche URL für die Rückkehr im Erfolgsfall, sowie im Stornierungsfall. Der Unterschied basiert nur auf unterschiedliche GET-Parametern.
Die Entscheidung ist natürlich Deine und sollte von der Softwarearchitektur Deines Systems abhängen. Deine Entscheidung über die URLs kann sich natürlich hier von unterscheiden.

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)));
}

Durch die Entscheidung eine URL mit unterschiedlichen GET-Parametern zu nutzen, wird 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 über GET an die Erfolgs-URL.

Je nach den GET-Werten wird zu einer anderen URL weitergeleitet. Im Erfolgsfall erfolgt eine Weiterleitung zum Controller für die Bearbeitung der Bezahlung. 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 Beispiel landet der Kunde auf der Payment-Seite, wenn etwas misslungen ist. Es gibt aber auch andere Shops, bei denen der Checkout damit beendet ist.
Je nachdem, wie Du Dich entscheidest, muss der Kunde den Warenkorb wieder füllen, oder eben nicht. Es hängt von Deinem Shop oder Deiner Entscheidung ab, wie verfahren soll.

Unsere Entscheidung kann ggf. die Abbruchrate eines Kaufes reduzieren. Schließlich gibt es einige Kunden, die nicht nochmals den Warenkorb füllen möchten.

Warum Du einen eigenen Controller für die Response 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 den Code schneller zu verstehen. Kommen nun immer mehr Bezahlmöglichkeiten hinzu, dann wird der Code immer größer.

Bezahldaten über Paypal REST API auslesen

Der Controller für die Bezahlabwicklung prüft die Daten 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 um die Rückgabewerte.

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 anschließend in die Datenbank gespeichert werden. Die Rückgabe von Paypal ist vom Typ Array. Um die Daten in ein Datenbank-Feld gespeichert werden können, wandeln wir die Daten in das JSON-Format um (s. Zeile 9). Dadurch ist der Code vor Änderungen der Schlüsselnamen geschützt. Wenn Du die Idee hast, jeden Datensatz in eine eigene Spalte zu speichern, könnten später ggf. Probleme entstehen. Wenn Paypal aus irgendeinem Grund die Schlüsselnamen ändert, müsste der Code im Shop angepasst werden.

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

Fazit

Der vorgestellte Code kann von allen genutzt werden um eine Paypal Integration in PHP durchzuführen. Du solltest aber darauf achten, dass Du den Code entsprechend an Dein System anpasst.

Solltest Du Fragen haben, dann kannst Du gerne einen Kommentar verfassen. Hast Du andere Anmerkungen oder Dir ist etwas unklar, dann schreib mir ebenso.