Paypal express checkout and drupal

Written by Nicholas on Wed, 03/20/2019 - 21:14

At flaircore.com, we love to interact with 3rd party api(s), in this blog we will explore how you could integrate paypal payments (express checkout) with your drupal website/application. We will create a custom drupal 8 module that will contain a form, and the logic required to integrate with the official paypa-php-sdk v1*

(you can get the code for this article on github here)

In our project root directory, under modules/ folder, create a folder named custom, and inside that folder create another folder named paypal_example. This folder will contain all our module files for this specific mission.

paypal_payment_example

 

Inside that folder, create the following files

  • paypal_example.info.yml (defines our module)
  • paypal_example.routing.yml (contains the route to our paypal checkout form as well as any other route we might need to define. )
  • create an 'src' directory and in it, another directory named 'Form', in this form directory create our payment form class, lets call it 'PaypalExample.php'.payment_example

    Below is the code paypal_example.info.yml should contain.

name: 'Paypal Example'
type: module
description: 'Contains example code on how to intergrate paypal payment with drupal 8'
core: 8.x
package: 'Example'

Below is the code paypal_example.routing.yml should contain

paypal_example.paypal_example:
  path: '/paypal_example/form'
  defaults:
    _form: '\Drupal\paypal_example\Form\PaypalExample'
    _title: 'PaypalExample'
  requirements:
    _permission: 'access content'

And , the code that should be in our src/Form/PaypalExample.php

but before adding the code in the file, make sure to download paypal-php-sdk via composer

<?php
 
namespace Drupal\paypal_example\Form;
 
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use PayPal\Api\Amount;
use PayPal\Api\Details;
use PayPal\Api\Item;
use PayPal\Api\ItemList;
use PayPal\Api\Payer;
use PayPal\Api\Payment;
use PayPal\Api\RedirectUrls;
use PayPal\Api\Transaction;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Paypal\Exception\PayPalConnectionException;
 
/**
 * Class PaypalExample.
 */
class PaypalExample extends FormBase {
 
 
  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'paypal_example';
  }
 
  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['amount'] = [
      '#type' => 'number',
      '#title' => $this->t('Amount'),
      '#description' => $this->t('The amount in store currency'),
      '#weight' => '0',
    ];
    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Pay with paypal'),
    ];
 
    return $form;
  }
 
  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    $amount = $form_state->getValue('amount');
    if (!$amount) {
      $form_state->setErrorByName('amount', t('Amount must be a valid value'));
      return;
    }
 
 
  }
 
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    /**
     * will update code from here
     */
    $storeCurrency = 'AUD';// set currency to pass
    if ($form_state->getValue('amount')) {
      // call our paypal express request passing the required vals
      $this->paypalExpressCheckoutRequest($form_state, $storeCurrency);
    }
 
 
 
  }
 
  /**
   * Request to paypal with our payment object
   */
 
  protected function paypalExpressCheckoutRequest(FormStateInterface $formState, $storeCurrency){
    // fill in our Test App credentials as created here
    // https://developer.paypal.com/developer/applications/
    $clientId = '';//Your own app
    $clientSecret = '';//Your own app
    $apiContext = new \PayPal\Rest\ApiContext(
      new \PayPal\Auth\OAuthTokenCredential(
        $clientId,
        $clientSecret
      )
    );
 
    $product_sku = 'test product 01';
    $product = $product_sku;
    $price = (float)$formState->getValue('amount');
    $shipping = 0.50;
    $total = $price + $shipping;
 
    // Create and fill in our payment object
    $payer = new Payer();
    $payer->setPaymentMethod('paypal');
 
    $item = new Item();
    $item->setName($product)
      ->setCurrency($storeCurrency)
      ->setQuantity(1)
      ->setPrice($price);
 
    $itemList = new ItemList();
    $itemList->setItems([$item]);
 
    $details =new Details();
    $details->setShipping($shipping)
      ->setSubtotal($price);
 
    $amount = new Amount();
    $amount->setCurrency($storeCurrency)
      ->setTotal($total)
      ->setDetails($details);
 
    $transaction = new Transaction();
    $transaction->setAmount($amount)
      ->setItemList($itemList)
      ->setDescription($product_sku)
      ->setInvoiceNumber(uniqid($product_sku));
 
    /**
     * define our route for paypal redirects first
     *  we will generate a url string from this current
     * route where the user is accessing the form, which is
     * also the route we defined in our routing.yml
     */
    $current_url = Url::fromRoute('paypal_example.paypal_example', [], ['absolute' => 'TRUE']);
    $urlString = $current_url->toString();
 
    $redirectUrls = new RedirectUrls();
    $redirectUrls->setReturnUrl($urlString. "/?success=true") // TODO, attach security token to url
      ->setCancelUrl($urlString. "/?success=false'");
 
    $payment = new Payment();
    $payment->setIntent('sale')
      ->setPayer($payer)
      ->setRedirectUrls($redirectUrls)
      ->setTransactions([$transaction]);
 
 
    try {
      $payment->create($apiContext);
 
    } catch (\Exception $exception) {
      die('Request failed '.$exception); //incase of error, output to user
    }
 
    $approvalUrl = $payment->getApprovalLink(); // Url to this payment
    $response = new TrustedRedirectResponse($approvalUrl); // redirect to external url
    $formState->setResponse($response);
 
    return;
  }
 
}

If you enable the module now, you can be able to access the example form via the route defined in the paypal_example.routing.yml i.e <yoursitename url>/paypal_example/form.

When you input a number  and click submit (Pay with paypal ) will be redirected to paypal, and if you log in and authorize the charge ('success'), you will be redirected to the same route you submitted the form from with some tokes of course in the url.

Our form works fine, but we need to process the paypal response (success= true or success=false) after the redirect from paypal.
To do this, we are going to create an event listener, to listen only to this url and check for marching tokens in it.
at the root of our module folder create a file named 'paypal_example.services.yml' and another file named PaypalListener.php and put it in our src folder.
below is the code required to be in the files.

#paypal_example.services.yml
services:
  paypal_example.paypal_listener:
    class: Drupal\paypal_example\PaypalListener
    arguments: []
    tags:
      - { name: event_subscriber }
<?php
/**
 * PaypalListener
 */
 
namespace Drupal\paypal_example;
 
 
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use PayPal\Api\Payment;
use PayPal\Api\PaymentExecution;
use PayPal\Rest\ApiContext;
use PayPal\Auth\OAuthTokenCredential;
 
class PaypalListener implements EventSubscriberInterface {
 
 
  public function paypalResponseEvent(GetResponseEvent $event){
    $request = $event->getRequest();
    /**
     * Check from the request if the route name matches what we
     * defined in our routing.yml, is for continue to process
     * the request
     */
    if ($request->attributes->get('_route') === 'paypal_example.paypal_example') {
 
      // get current route for use in redirects
      $current_url = Url::fromRoute('paypal_example.paypal_example', [], ['absolute' => 'TRUE']);
      $urlString = $current_url->toString();
 
      if ($request->query->get('success') && $request->query->get('paymentId') && $request->query->get('PayerID')) {
        $paypalSuccessResponse = $request->query->get('success');
        if ($paypalSuccessResponse === 'false') {
          // clear the temporary values in the Url by redirect the user
          $response = new RedirectResponse($urlString);#same page
          $response->send();
          \Drupal::messenger()->addMessage('Failed to authorize charge/payment');
 
          return;
        }
 
        if ($paypalSuccessResponse === 'true') {
 
          $paymentId = $request->query->get('paymentId');
          $payerId = $request->query->get('PayerID');
 
          // fill in our Test App credentials as created here
          // https://developer.paypal.com/developer/applications/
          $clientId = '';//Your own app
          $clientSecret = '';// Your own app
          $apiContext = new ApiContext(
            new OAuthTokenCredential(
              $clientId,
              $clientSecret
            )
          );
 
          /**
           * create our payment object to send to paypal
           * if success, we will use it's data
           */
          $payment = Payment::get($paymentId, $apiContext);
          $execute = new PaymentExecution();
          $execute->setPayerId($payerId);
 
 
          try {
 
            $payment->execute($execute, $apiContext);
 
          } catch (\Exception $exception) {
            // maybe log errors here and choose what to pass on to display to user
 
            // if we have an error display message and return
            // we should still clear the values in the url here
            // but to avoid code repetition and to stick to the
            // scope of this article, I will leave that out.
            if ($exception) {
              \Drupal::messenger()->addMessage('Error charging you!!: '.$exception->getMessage());
              return;
            }
          }
          /**
           * if no exception go ahead and save something(s) to the database,
           * clear temporary values in the url, and thank the user.
           * you can dump($payment) below and choose what to post to database
           *
           */
          $paymentState = $payment->getState();
 
          if ($paymentState === 'approved') {
            $amount = $payment->transactions[0]->amount->total;
 
 
            // clear the temporary values in the Url by redirect the user
 
            $response = new RedirectResponse($urlString);#same page but cleared url vals
            $response->send();
 
            \Drupal::messenger()->addMessage('Thank you!, Payment received for amount: '.$amount . ' AUD.');
 
            return;
          }
        }
      }
    }
  }
 
  public static function getSubscribedEvents() {
    return [
      KernelEvents::REQUEST => 'paypalResponseEvent', // implement paypalResponseEvent method above
    ];
  }
}

If everything is as it should be, you should have everything specified above working (remember we are using v1* of the paypal-php-sdk), but it doesn't post anything to the database yet, however you can implement such a feature and choose what to save from the payment object(like amount).

Blogs