PHP - Stripe Payment

Created At: 2023-10-09 04:51:06 Updated At: 2023-10-21 21:08:20

Here we will take a detail look how to create payment api using PHP and integrate with Stripe payment.We would do that based on Laravel framework. But the core principle of PHP is still applied to it. So if you use vanilla PHP or other PHP framework like symphony, idea still stays the same. 

We wrote this tutorial step by step with given index so that you can follow it easily. You may click on, any of the below link and jump around for easy navigation. 

Index of this tutorial

Server setup

Setup payment endpoints

Payment flow chart

Complete code

Payment related table

Get Stripe key

Check existing products and orders

Payment webview

Web go hooks for Stripe

Server setup

You need to make sure you have a Laravel or PHP server running. The payment logic would work for any nodejs server as well. After that you need to make sure you can set up endpoint correctly. You may learn about Laravel setup from the link.

At the same, we need to install Stripe SDK. That' why we would need to run the below command

composer require stripe/stripe-php

It would install the necessary packages in the vendor folder. You may look for vender/Stripe folder.

Setup payment endpoints

Now time to setup our payment endpoints. There are two endpoints that we need to setup. One for placing order and other one for getting payment related information as web hook. We need web hook type endpoint, because we want to get information from Stripe and update our database.

Here I pointed the endpoints as checkout and webGoHooks. We also assume that we create this PaymentController and the controller is put inside Api folder. 

In your case, you may call the controller anything and place it anywhere. And then we made sure we guard our checkout api using middleware which means, before making a payment, user needs to login. 

For this authentication purpose, we have used Sanctum package. If you don't know how to use Sanctum package, check out it here.

The last endpoint is not projected by Sanctum, because that would be publicly accessible from third party server.

Flow chart

The below flow chart of Stripe payment has been clearly explained in this video tutorial as well. If you follow this tutorial with the video one, you will get best out of it. You may find it from the link below.

Stripe payment api

You above picture, you may also get the watermark free version just for a dollar on buymeacoffe link.

Take the courses to master api building

Flutter and Laravel course using GetX

Flutter and Laravel course using BLoC

Flutter and Laravel course using Riverpod

React Native and Node Js course

Complete code

Below is the complete code. After the code there is also a step by step explanation. If you follow the explanation both from video and text, you may build the same api using other technology like Nodejs, Python or Java. The principles for Stripe payment api is same for all the languages or framework.

class PayController extends Controller
{
    //
    public function checkout(Request $request){
     try{
            $user = $request->user();
            $token = $user->token;
            $courseId= $request->id;

            /*
                Stripe api key
            */
            Stripe::setApiKey('sk_test_51NDjUSDcNOyMHK5HXM82Vp9SmGNrUbu4wTpn4KsvytjoVxnJXo6243K262SqdHHypMIloirm1xSVEnqW0edTSH1N00h9q3RWf3');


            $courseResult = Course::where('id', '=', $courseId)->first();
            //invalid request
            if(empty($courseResult)){
                return response()->json([
                    'code' => 400,
                    'msg' => "Course does not exist",
                    'data'=>''
                ], 400);
            }

            $orderMap = [];

            $orderMap['course_id']= $courseId;
            $orderMap['user_token']=$token;
            $orderMap['status'] = 1;

            /*
                if the order has been placed before or not
                So we need Order model/table
            */

            $orderRes = Order::where($orderMap)->first();
            if(!empty($orderRes)){
                return response()->json([
                    'code' => 400,
                    'msg' => "You already bought this course",
                    'data' => ""
                ],);
            }
            //new order for the user and let's submit
            $YOUR_DOMAIN = env('APP_URL');
            $map = [];
            $map['user_token'] = $token;
            $map['course_id'] = $courseId;
            $map['total_amount'] = $courseResult->price;
            $map['status'] = 0;
            $map['created_at'] = Carbon::now();
            $orderNum = Order::insertGetId($map);
            //create payment session

            $checkOutSession = Session::create(
                [
                    'line_items'=>[[
                        'price_data'=>[
                            'currency'=>'USD',
                            'product_data'=>[
                                'name'=>$courseResult->name,
                                'description'=>$courseResult->description,
                            ],
                            'unit_amount'=>intval(($courseResult->price)*100),
                        ],
                        'quantity'=>1,
                    ]],
                    'payment_intent_data'=>[
                        'metadata'=>['order_num'=>$orderNum, 'user_token'=>$token],
                    ],
                    'metadata' => ['order_num' => $orderNum, 'user_token' => $token],
                    'mode'=>'payment',
                    'success_url'=> $YOUR_DOMAIN . 'https://5bf1-45-158-180-176.ngrok-free.app/success',
                    'cancel_url'=> $YOUR_DOMAIN . 'https://5bf1-45-158-180-176.ngrok-free.app/cancel'
                ]
            );

            //returning stripe payment url
            return response()->json([
                'code' => 200,
                'msg' => "success",
                'data'=>$checkOutSession->url
            ], 200);
     }catch(\Throwable $th){


            return response()->json([
                'error' => $th->getMessage(),
            ], 500);
     }
    }

}

In our php file at the top, we need to import related packages. 

<?php

namespace App\Http\Controllers\Api;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Carbon;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Stripe\Webhook;
use Stripe\Customer;
use Stripe\Price;
use Stripe\Stripe;
use Stripe\Checkout\Session;
use Stripe\Exception\UnexpectedValueException;
use Stripe\Exception\SignatureVerificationException;
use App\Models\Course;
use App\Models\Order;

class PaymentController extends Controller
{
.......................................................................

 

In the above class we have a method name checkout(). It's the one that we will talking about it. If you are excited about it, you can go ahead use the method for Laravel framework. Here we will assume you have two tables in the database

1. Course table-> it would be like our product table. Each course or product is saved in this table.

2. Order table -> it would save our placed orders. All the successful orders are saved here.

Here we assume that you will have a product table. In our case the table name is Course. You also need to create an Order table. 

Here we also assume that you will send a product id from the api http request. That would be our product id. In this article we are assuming this product id is course_id. So we saved it in a variable name $course_id.

So from the $request (instance of Request) object we get the product id as $request->id. Let's see the code below for this section

$user = $request->user();
$token = $user->token;
$courseId= $request->id;

$request object which comes from Request class, captures all the incoming http parameters. it also gets user information using $request->user().

Get Stripe key

Next we used Stripe::setApiKey() to use the Stripe test key from the Stripe console. Let's see from the picture how to get the Strip publishable test key.

Go to developers section and the get the API keys. First we interested in Secret key numbered by 3. Get the key and put inside Stripe::setApiKey().

Check existing products and orders

Next we see the 

 $courseResult = Course::where('id', '=', $courseId)->first();

Here first we check if the $courseId(like our product id) exist in the Courses table(Laravel eloquent is Course). And then we simply return if the product id does not exist in the database.

Then we check if the same order exist in the database or not. The below code checks it

            $orderMap = [];

            $orderMap['course_id']= $courseId;
            $orderMap['user_token']=$token;
            $orderMap['status'] = 1;

            /*
                if the order has been placed before or not
                So we need Order model/table
            */

            $orderRes = Order::where($orderMap)->first();
            if(!empty($orderRes)){
                return response()->json([
                    'code' => 400,
                    'msg' => "You already bought this course",
                    'data' => ""
                ],);
            }

All of our orders are saved in the Order table. If we already bought the same course( our product) then we just return.

Then we proceed to make our actual order placement. 

 //new order for the user and let's submit
$YOUR_DOMAIN = env('APP_URL');
$map = [];
$map['user_token'] = $token;
$map['course_id'] = $courseId;
$map['total_amount'] = $courseResult->price;
$map['status'] = 0;
$map['created_at'] = Carbon::now();
$orderNum = Order::insertGetId($map);

Here we prepared information to place in our Order table.  We insert user token as user identity, course id as our product id, course price as our product price and status. The last line is the one that inserts order in the Order table and for that we use insertGetId(). This insertGetId() method returns the inserted id from the table. It would be inserted ID of row.

Here $map['status']=0 is the most important one. Because initially when we place an order information in the table and we set status=0, the reason is that after placing an order in the Order table we go for making payment. The payment might fail. If the payment is successful we will update $map['status'] to 1.

Payment webview

And then the actual Stripe payment happens through a form submission

            $checkOutSession = Session::create(
                [
                    'line_items'=>[[
                        'price_data'=>[
                            'currency'=>'USD',
                            'product_data'=>[
                                'name'=>$courseResult->name,
                                'description'=>$courseResult->description,
                            ],
                            'unit_amount'=>intval(($courseResult->price)*100),
                        ],
                        'quantity'=>1,
                    ]],
                    'payment_intent_data'=>[
                        'metadata'=>['order_num'=>$orderNum, 'user_token'=>$token],
                    ],
                    'metadata' => ['order_num' => $orderNum, 'user_token' => $token],
                    'mode'=>'payment',
                    'success_url'=> $YOUR_DOMAIN . '/success,
                    'cancel_url'=> $YOUR_DOMAIN . '/cancel'
                ]
            );

Here Session::create() is core of the Stripe api that communicates with Stripe server and helps us placing order information. It will return us a payment url which we would fill in information of our credential like credit card information and name. 

This returned webview would also show us the product basic information like company name, product name and production discritpion with charge(an amount of the order).

Here a few things note about the numbers on the picture

  1. Your app or organizaiton name
  2. Your product name
  3. Product price
  4. Product discription 
  5. Your user input information

If the above items don't make sense to you, you must watch the video tutorial. The above tutorial is over 2 hours long. It's impossible to explain everything in a short text.

Once it's done successfully, it will return 'success_url' property. Session::create() takes an array, in fact it's an associative array.

They are here associative array inside array. Their Stripe link has enough information about these fields. Here let me explain 

'unit_amount'=>intval(($courseResult->price)*100),

Here you might see that we are multiplying the price with 100, that's because USD in Stripe is calculated using cents. So we have multiply it using 100 since I USD = 100 cents.

Build api with front end flutter app

If you are able to run the api, on post successfully, you will get a link. That link is from Stripe and it's a payment link UI. You may see it from the picture below

Web go hooks for Stripe

Time for setting up our webGoHooks api endpoint. To do that, you need to go to your developers account and then click WebGoHooks and then click addEndPoints

And then you need to setup the endpoint correctly. It's very important that you don't make mistakes.

Here we used the endpoint name which we set up very early in the tutorial.

After form submission it may look like the payment is done. Actually the payment is done. We need to take care of some after sales service. We would need to update our database Order table. Our Order table has a field name status. Originally it's status=0, which means payment was not done yet.

Even before that, we need to set up our web hook secrets in the code. That's what we have done in the code as you can see from $endPointSecret variable.

If the payment is done, we need to change back the status=1.  The code for this would look ike this

public function webGoHooks(){
        Log::info('starts here.....');
        Stripe::setApiKey('sk_test_51NDjUSDcNOyMHK5HXM82Vp9SmGNrUbu4wTpn4KsvytjoVxnJXo6243K262SqdHHypMIloirm1xSVEnqW0edTSH1N00h9q3RWf3');
        $endPointSecret = 'whsec_EFiHVvfetmCEjjIC6aRiJ9B3sUGLrD06';
        $payload = @file_get_contents('php://input');
        $sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'];
        $event = null;
        Log::info('set up buffer and handshake done.....');
        try{
           $event =  \Stripe\Webhook::constructEvent(
                    $payload,
                    $sigHeader,
                    $endPointSecret
            );

        }catch(\UnexpectedValueException $e){
            Log::info('UnexpectedValueException '.$e);
            http_response_code(400);
            exit();
        }catch(\Stripe\Exception\SignatureVerificationException $e){
            Log::info('SignatureVerificationException '.$e);
            http_response_code(400);
            exit();
        }

        if($event->type=="charge.succeeded"){
            $session = $event->data->object;
            $metadata = $session["metadata"];
            $orderNum = $metadata->order_num;
            $userToken = $metadata->user_token;
            Log::info('order id'.$orderNum);
            $map = [];
            $map['status'] = 1;
            $map['updated_at'] = Carbon::now();
            $whereMap = [];
            $whereMap['user_token'] = $userToken;
            $whereMap['id']=$orderNum;
            Order::where($whereMap)->update($map);
        }

        http_response_code(200);
    }

You need make sure you register this method as an endpoint in your routes.php file. If our payment is successful, then we will get charge.succeeded object from Stripe.

If we get charge.succeeded object, then we update our database filed through Order table.

Comment

Add Reviews