PayPal Integration with PHP

PayPal Integration – Omnipay PayPal PHP Library v3.0 with Laravel

This is my third blog post about PayPal Integration with Omnipay payment processing library. In this article, I will mainly cover about the recent release v3.0, which is finally available for developers around the world. I'm sure many developers around the world were eagerly waiting for this release.

In my previous article I explained about how to Integrate Omnipay v2.* with Laravel framework.

In the new release v3.0, the team focused mainly on separating the HTTP Client, to be independent with Guzzle. This release now supports symfony 3,4 components to run with all Laravel v5.* versions. This is a most awaited release, developers were not able to use, as it was not fully compatible with latest symfony components.

To know about the breaking changes and new additions to the package, view the full release note from their official doc and github repository.

The original omnipay/omnipay package has been changed as league/omnipay

By using this package (omnipay/paypal) we can implement different gateways listed below.

  • PayPal_Express (PayPal Express Checkout)
  • PayPal_ExpressInContext (PayPal Express In-Context Checkout)
  • PayPal_Pro (PayPal Website Payments Pro)
  • PayPal_Rest (Paypal Rest API)

We're going to integrate PayPal Express Checkout in this article below.

Before starting the coding let's install the package via composer.

composer require league/omnipay omnipay/paypal

Let's add new routes for the PayPal integration under app/routes/web.php

Route::get('/paypal/{order?}', [
    'name' => 'PayPal Express Checkout',
    'as' => 'order.paypal',
    'uses' => 'PayPalController@form',
]);

Route::post('/checkout/payment/{order}/paypal', [
    'name' => 'PayPal Express Checkout',
    'as' => 'checkout.payment.paypal',
    'uses' => 'PayPalController@checkout',
]);

Route::get('/paypal/checkout/{order}/completed', [
    'name' => 'PayPal Express Checkout',
    'as' => 'paypal.checkout.completed',
    'uses' => 'PayPalController@completed',
]);

Route::get('/paypal/checkout/{order}/cancelled', [
    'name' => 'PayPal Express Checkout',
    'as' => 'paypal.checkout.cancelled',
    'uses' => 'PayPalController@cancelled',
]);

Route::post('/webhook/paypal/{order?}/{env?}', [
    'name' => 'PayPal Express IPN',
    'as' => 'webhook.paypal.ipn',
    'uses' => 'PayPalController@webhook',
]);

I mainly write named routes all over my applications, having name routes are easy to create URLs from anywhere within the project with a simple helper route(), I have mentioned before about this mechanism with my previous articles as well.

Adding required models for this implementation.

app/Order.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    const PAYMENT_COMPLETED = 1;
    const PAYMENT_PENDING = 0;

    /**
     * @var string
     */
    protected $table = 'orders';

    /**
     * @var array
     */
    protected $dates = ['deleted_at'];

    /**
     * @var array
     */
    protected $fillable = ['transaction_id', 'amount', 'payment_status'];
}

The order model only has few basic fields implemented just for the simple example, you are free to customize with your required fields in your application.

Adding new database table orders, you need to create a migration file with a console command.

php artisan make:migration create_table_orders --create=orders
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateOrdersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->increments('id');
            $table->string('transaction_id')->nullable();
            $table->float('amount')->unsigned()->nullable();
            $table->integer('payment_status')->unsigned()->default(0);
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('orders');
    }
}

Adding new helper class for generating gateway object and routes to interact with PayPal API.

app/PayPal.php

<?php

namespace App;

use Omnipay\Omnipay;

/**
 * Class PayPal
 * @package App
 */
class PayPal
{
    /**
     * @return mixed
     */
    public function gateway()
    {
        $gateway = Omnipay::create('PayPal_Express');

        $gateway->setUsername(config('services.paypal.username'));
        $gateway->setPassword(config('services.paypal.password'));
        $gateway->setSignature(config('services.paypal.signature'));
        $gateway->setTestMode(config('services.paypal.sandbox'));

        return $gateway;
    }

    /**
     * @param array $parameters
     * @return mixed
     */
    public function purchase(array $parameters)
    {
        $response = $this->gateway()
            ->purchase($parameters)
            ->send();

        return $response;
    }

    /**
     * @param array $parameters
     */
    public function complete(array $parameters)
    {
        $response = $this->gateway()
            ->completePurchase($parameters)
            ->send();

        return $response;
    }

    /**
     * @param $amount
     */
    public function formatAmount($amount)
    {
        return number_format($amount, 2, '.', '');
    }

    /**
     * @param $order
     */
    public function getCancelUrl($order)
    {
        return route('paypal.checkout.cancelled', $order->id);
    }

    /**
     * @param $order
     */
    public function getReturnUrl($order)
    {
        return route('paypal.checkout.completed', $order->id);
    }

    /**
     * @param $order
     */
    public function getNotifyUrl($order)
    {
        $env = config('services.paypal.sandbox') ? "sandbox" : "live";

        return route('webhook.paypal.ipn', [$order->id, $env]);
    }
}

Laravel ships with a config file config/services.php to store credentials to be used with third-party API services. For this implementation add following credentials under the same file like below.

    'paypal' => [
        'username' => env('PAYPAL_USERNAME'),
        'password' => env('PAYPAL_PASSWORD'),
        'signature' => env('PAYPAL_SIGNATURE'),
        'sandbox' => env('PAYPAL_SANDBOX'),
    ],

To handle and maintain the request coming into the application we need to pass information to the controller. We are adding new PayPalController.php

<?php

namespace App\Http\Controllers;

use App\Order;
use App\PayPal;
use Illuminate\Http\Request;

/**
 * Class PayPalController
 * @package App\Http\Controllers
 */
class PayPalController extends Controller
{
    /**
     * @param Request $request
     */
    public function form(Request $request)
    {
        $order = Order::findOrFail(mt_rand(1, 140));

        // the above order is just for example.

        return view('form', compact('order'));
    }

    /**
     * @param $order_id
     * @param Request $request
     */
    public function checkout($order_id, Request $request)
    {
        $order = Order::findOrFail(decrypt($order_id));

        $paypal = new PayPal;

        $response = $paypal->purchase([
            'amount' => $paypal->formatAmount($order->amount),
            'transactionId' => $order->id,
            'currency' => 'USD',
            'cancelUrl' => $paypal->getCancelUrl($order),
            'returnUrl' => $paypal->getReturnUrl($order),
        ]);

        if ($response->isRedirect()) {
            $response->redirect();
        }

        return redirect()->back()->with([
            'message' => $response->getMessage(),
        ]);
    }

    /**
     * @param $order_id
     * @param Request $request
     * @return mixed
     */
    public function completed($order_id, Request $request)
    {
        $order = Order::findOrFail($order_id);

        $paypal = new PayPal;

        $response = $paypal->complete([
            'amount' => $paypal->formatAmount($order->amount),
            'transactionId' => $order->id,
            'currency' => 'USD',
            'cancelUrl' => $paypal->getCancelUrl($order),
            'returnUrl' => $paypal->getReturnUrl($order),
            'notifyUrl' => $paypal->getNotifyUrl($order),
        ]);

        if ($response->isSuccessful()) {
            $order->update([
                'transaction_id' => $response->getTransactionReference(),
                'payment_status' => Order::PAYMENT_COMPLETED,
            ]);

            return redirect()->route('order.paypal', encrypt($order_id))->with([
                'message' => 'You recent payment is sucessful with reference code ' . $response->getTransactionReference(),
            ]);
        }

        return redirect()->back()->with([
            'message' => $response->getMessage(),
        ]);
    }

    /**
     * @param $order_id
     */
    public function cancelled($order_id)
    {
        $order = Order::findOrFail($order_id);

        return redirect()->route('order.paypal', encrypt($order_id))->with([
            'message' => 'You have cancelled your recent PayPal payment !',
        ]);
    }

        /**
     * @param $order_id
     * @param $env
     * @param Request $request
     */
    public function webhook($order_id, $env, Request $request)
    {
        // to do with new release of sudiptpa/paypal-ipn v3.0 (under development)
    }
}

Finally, adding a simple view to submitting a form to pay the order amount with PayPal, the form redirects the user to PayPal hosted page to enter the user account details for the security reason.

app/resources/views/form.blade.php

@extends('app')

@section('content')
    <div class="container">
        <div class="gateway--info">
            <div class="gateway--desc">
                @if(session()->has('message'))
                    <p class="message">
                        {{ session('message') }}
                    </p>
                @endif
                <div class="row">
                    <div class="col">
                        <img src="{{ asset('images/paypal.png') }}" class="img-responsive gateway__img">
                    </div>
                    <div class="col">
                        <img src="{{ asset('images/laravel.png') }}" class="img-responsive gateway__img">
                    </div>
                </div>
                <p><strong>Order Overview !</strong></p>
                <hr>
                <p>Item : Yearly Subscription cost !</p>
                <p>Amount : ${{ $order->amount }}</p>
                <hr>
            </div>
            <div class="gateway--paypal">
                <form method="POST" action="{{ route('checkout.payment.paypal', ['order' => encrypt(mt_rand(1, 140))]) }}">
                    {{ csrf_field() }}
                    <button class="btn btn-pay">
                        <i class="fa fa-paypal" aria-hidden="true"></i> Pay with PayPal
                    </button>
                </form>
            </div>
        </div>
    </div>
@stop

The preview of the above view looks like below, I just made for this example.

PayPal Integration with PHP

For the above real-time example, I tried to cover only the mechanism to handle the payment request, failed payment, canceled a payment. You are free to customize it and manage as per your application architecture. The above example was created with Laravel v5.6 version but it works for all v5.* versions, that's the benefit of the new release for omnipay/paypal latest version v3.0.

To view the full source codes, view few commits (65e4b5a, 75a9585, c469e59, ab0b4db b24fa97) on github example repository.

Thanks for reading this article up on the end, please leave your feedback below in the comment section.

Happy Coding!