Laravel Stripe Payment Gateway Integration

0
2345
Laravel Stripe Payment Gateway Integration

# Install Laravel 8

Before digging deeper into laravel stripe payment gateway integration, you must have basic knowledge of the laravel framework. Throughout this tutorial, I’ll use Laravel 8.

Install the laravel framework, using the following commands. That will start your development server at http://localhost:8000.

composer create-project laravel/laravel stripe
cd stripe
php artisan serve

# Install Required Dependencies

Next, we need some composer dependencies. Laravel provides a powerful package called laravel/cashier for handling stripe’s functionality. Also, for the flash messages, we’ll use laracast/flash package as well.

=> Installing laravel/cashier

First of all, install the cashier package with the following command. After that, you need to put STRIPE_KEY and STRIPE_SECRET in your .env file. You need to create an account in the stripe. Finally, you’ll get your stripe keys in the dashboard. Make sure, you are using test credentials for the demo purpose.

composer require laravel/cashier
STRIPE_KEY=pk_test_TYooMQauvdEDq54NiTphI7jx
STRIPE_SECRET=sk_test_4eC39HqLyjWDarjtT1zdp7dc

=> Installing laracasts/flash

Another powerful package is laracasts/flash. This package provides bootstrap optimized flash messaging. It’s a good idea to use this package in every new application. After installing laracast/flash package, we need to configure where to put flash messages in the master layout. But we’ll do it later.

composer require laracasts/flash

# Configure Database & Seeding Products

Configure database credentials by putting credentials into the .env file.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=stripe
DB_USERNAME=root
DB_PASSWORD=

=> Seeding Products Into Database

Generally speaking, seeding the database is the best practice in every application. I’ll use the Product factory to seed products in the database. For that, we need a Product model, migration, controller, and the factory. Let’s do it in a shorter way.

php artisan make:model Product -mcf

The above command will create the Product model, ProductController, Product migration, and ProductFactory with a single command.

Create a products table schema by amending a product migration.

/* database > migrations > *_create_products_table.php */

<?php

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

class CreateProductsTable extends Migration
{  
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('description');
            $table->float('price');
            $table->string('image');
            $table->timestamps();
        });
    }
 
    public function down()
    {
        Schema::dropIfExists('products');
    }
}

In the middle, it’s time to write a factory using the laravel faker package.

/* database > factories > ProductFactory.php */

<?php

namespace Database\Factories;

use App\Models\Product;
use Illuminate\Database\Eloquent\Factories\Factory;

class ProductFactory extends Factory
{    
    protected $model = Product::class;
        
    public function definition()
    {
        return [
            'name' => $this->faker->words(2, true),
            'description' => $this->faker->sentence,
            'price' => rand(100, 999),
            'image' => $this->faker->imageUrl(400, 250)
        ];
    }
}

Last, Open the DatabaseSeeder.php and put the following code.

/* database > seeders > DatabaseSeeder.php */

<?php

namespace Database\Seeders;

use App\Models\Product;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{ 
    public function run()
    {
        Product::factory(8)->create();
    }
}

I think 8 products are more than enough for this tutorial. Let’s complete this seeding with the following command.

php artisan migrate
php artisan db:seed

//Or you can use following command to reset & recreate whole database 
php artisan migrate:refresh --seed

# Creating Necessary Files

Before creating a necessary files, let’s create a resources > views > layouts > master.blade.php layout first.

/* resources > views > layouts > master.blade.php */

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Stripe Payment Gateway Integration In Laravel - Coding Lesson</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
    @stack('header')
</head>
<body>

<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <a class="navbar-brand" href="#">{{ config('app.name') }}</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarNav">
        <ul class="navbar-nav">
            <li class="nav-item @if(Route::currentRouteName()=='products') active @endif">
                <a class="nav-link" href="{{ route('products') }}">Products</a>
            </li>
            <li class="nav-item @if(Route::currentRouteName()=='cart') active @endif">
                <a class="nav-link" href="{{ route('cart') }}">Cart <i class="fa fa-cart-arrow-down"></i> <sup><span class="badge badge-primary">{{ \App\Models\CartItem::sum('quantity') }}</span></sup></a>
            </li>
        </ul>
    </div>
</nav>

<div class="container mt-5">
    @include('flash::message')
    @yield('content')
</div>

<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script>
@stack('footer')

<script>
    $('div.alert').not('.alert-important').delay(3000).fadeOut(350);
</script>
</body>
</html>

=> Creating Routes

As you can see below, there are lots of routes for this application. The first two routes are for products page and products add to cart functionality. The third and the fourth routes are for the cart page and remove the product from the cart functionality. The checkout route is main route for stripe checkout functionality. Also, the success and the cancel route are necessary as per stripe documentation.

<?php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::group(['namespace' => 'App\Http\Controllers'], function() {
    Route::get('products', 'ProductController@index')->name('products');
    Route::get('products/{product}/addToCart', 'ProductController@addToCart')->name('products.addToCart');
    Route::get('cart', 'CartController@index')->name('cart');
    Route::get('cart/{product}/remove', 'CartController@removeProduct')->name('cart.removeProduct');
    Route::post('checkout', 'CheckoutController@checkout')->name('checkout');
    Route::get('success', 'CheckoutController@success')->name('success');
    Route::get('cancel', 'CheckoutController@cancel')->name('cancel');
});

=> Creating Models, Controllers And Migrations

In this tutorial, we need lots of modes, controllers, and pages. So Let’s create it.

php artisan make:model Cart -mc
php artisan make:model CartItem -m
php artisan make:controller CheckoutController

Next, let’s create carts and cart_items schema by amending the following files. Also, migrate it then.

/* database > migrations > *_create_carts_table.php */

<?php

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

class CreateCartsTable extends Migration
{   
    public function up()
    {
        Schema::create('carts', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('carts');
    }
}
/* database > migrations > *_create_cart_items_table.php */

<?php

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

class CreateCartItemsTable extends Migration
{
    public function up()
    {
        Schema::create('cart_items', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('cart_id');
            $table->foreign('cart_id')->references('id')->on('carts');
            $table->unsignedBigInteger('product_id');
            $table->foreign('product_id')->references('id')->on('products');
            $table->float('price');
            $table->unsignedBigInteger('quantity');
            $table->float('total');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('cart_items');
    }
}
php artisan migrate

# Products View Page Implementation

To implement, products page, open the ProductController, and create two methods. The index method will return the products page, which we will create later in this tutorial. Likewise, the addToCart method will add the product to the cart.

The addToCart method will check the product exists or not in the cart items. If the product exists, then it will increase the only quantity of the specific item. Or else, it will create a new item in the cart items.

/* app > Http > Controllers > ProductController.php */

<?php

namespace App\Http\Controllers;

use App\Models\Cart;
use App\Models\CartItem;
use App\Models\Product;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    public function index() {
        $products = Product::all();
        return view('products', compact('products'));
    }

    public function addToCart(Product $product) {

        // Create or Update Cart
        $cart = Cart::updateOrCreate(['id' => 1]);

        // Update Cart items
        $existProduct = CartItem::where('product_id', $product->id)->first();
        if($existProduct) {
            CartItem::where('product_id', $product->id)->update([
                'quantity' => ++$existProduct->quantity,
                'total' => $existProduct->quantity * $existProduct->price,
            ]);
        } else {
            $cart->cart_items()->create([
                'product_id' => $product->id,
                'price' => $product->price,
                'quantity' => 1,
                'total' => $product->price
            ]);
        }

        flash('Product added to the cart successfully!');
        return redirect()->back();
    }
}

Next, add relationships between the Product, Cart, and CartItem model. The Cart model hasMany cart items. Similarly, CartItem model belongsTo cart and the product.

/* app > Models > Cart.php */

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Cart extends Model
{
    use HasFactory;
    protected $fillable = ['id'];

    public function cart_items() {
        return $this->hasMany('App\Models\CartItem');
    }
}
/* app > Models > CartItem.php */

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class CartItem extends Model
{
    use HasFactory;
    protected $fillable = ['cart_id', 'product_id', 'price', 'quantity', 'total'];

    public function cart() {
        return $this->belongsTo('App\Models\Cart');
    }

    public function product() {
        return $this->belongsTo('App\Models\Product');
    }
}

Last, Create resources > views > products.blade.php file.

/* resources > views > products.blade.php */

@extends('layouts.master')
@section('content')

    @foreach($products->chunk(4) as $chunk)
    <div class="row">
       @foreach($chunk as $product)
            <div class="col-md-3">
                <div class="card">
                    <img src="{{ $product->image }}" class="card-img-top" alt="{{ $product->name }}">
                    <div class="card-body">
                        <h5 class="card-title">{{ $product->name }}</h5>
                        <p class="card-text h-50">{{ $product->description }}</p>
                        <a href="{{ route('products.addToCart', $product->id) }}" class="btn btn-info btn-block"><i class="fa fa-cart-plus"></i> Add To Cart</a>
                    </div>
                </div>
            </div>
       @endforeach
    </div> <br>
    @endforeach
@endsection

Also modify, welcome.blade.php as below to see navigation properly. Now, open your browser and visit http://localhost:8000/products.

@extends('layouts.master')
@section('content')
    <div class="row">
        <div class="col-md-12">
            <div class="alert alert-info">
                <h4 class="alert-heading">Welcome, to Coding Lesson</h4>
                <p>In this tutorial you'll learn stripe payment gateway implementation in laravel.</p>
            </div>
        </div>
    </div>
@endsection
Stripe Implementation Products Page

Now, click on Add To Cart button. It will add that product to the cart.

# Carts Page Implementation

In addition, let’s create a cart page. To do that, open CartController create two methods. The index method will return the cart page. Identically, the removeProduct method will remove product from the cart page.

/* app > Http > Controllers > CartController.php */

<?php

namespace App\Http\Controllers;

use App\Models\CartItem;
use App\Models\Product;
use Illuminate\Http\Request;

class CartController extends Controller
{
    public function index() {
        $cartItems = CartItem::with('product')->get();
        return view('cart', compact('cartItems'));
    }

    public function removeProduct(Product $product){
        CartItem::where('product_id', $product->id)->delete();
        flash('Product removed from the cart successfully!');
        return redirect()->back();
    }
}

Besides, create resources > views > cart.blade.php file and modify it below.

/* resources > views > cart.blade.php */

@extends('layouts.master')
@section('content')
    <table class="table table table-striped">
        <thead>
        <tr>
            <th>Product</th>
            <th>Image</th>
            <th>Price</th>
            <th>Quantity</th>
            <th>Total</th>
            <th></th>
        </tr>
        </thead>
        <tbody>
        @forelse($cartItems as $cartItem)
            <tr>
                <td>{{ $cartItem->product->name }}</td>
                <td><img src="{{ $cartItem->product->image }}" class="img-thumbnail" style="height:50px;"></td>
                <td>{{ number_format($cartItem->price, 2) }}</td>
                <td>{{ $cartItem->quantity }}</td>
                <td>{{ number_format($cartItem->total, 2) }}</td>
                <td><a href="{{ route('cart.removeProduct', $cartItem->product_id) }}" class="text-danger">X</a></td>
            </tr>
        @empty
            <tr>
                <td colspan="6" align="center">No products found.</td>
            </tr>
        @endforelse

        </tbody>
        @if($cartItems->count() > 0)
            <tfoot>
            <tr>
                <th colspan="3"></th>
                <th>Grand Total</th>
                <th>{{ number_format($cartItems->sum('total'), 2) }}</th>
                <th></th>
            </tr>
            </tfoot>
        @endif
    </table>

    @if($cartItems->count() > 0)
        <button class="btn btn-primary btn-block" id="checkout-button"><i class="fa fa-cc-stripe"></i> Pay {{ number_format($cartItems->sum('total'), 2) }}</button>
    @endif

@endsection
Stripe Implementation Cart Page

# Implementing Stripe & Accept Payment

As you can see, on the cart page, there is a button with an id called checkout-button. Keep in mind, clicking on this button will create a stripe session. After creating a stripe session, it will call the checkout route with ajax. With this in mind, I’ve created a checkout route.

=> Excluding Routes For Checkout

To clarify, any POST routes require a CSRF token by default. Stripe can not access the checkout route without a CSRF token. Therefore, you need to add this route as an exception. There are two ways to do that.

In the first method, you can add a CSRF token in a meta tag in the head. Now, you need to call $.ajaxSetup in the footer section before the body tag. Now, every POST request will automatically receive a CSRF token by default. To achieve that, follow this procedure.

On the other hand, you can the checkout route in VerifyCsrfToken middleware as below.

/* app > Http > Middleware > VerifyCsrfToken.php */

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    protected $except = [
        'checkout'
    ];
}

=> Creating Stripe Session

For the stripe session, amend the cart.blade.php. As you can see, there is a button click event with an ajax call. In conclusion, it will call the checkout route. Moreover, that function requires a stripe publishable key. Your whole cart.blade.php will look like below.

/* resources > views > cart.blade.php */

@extends('layouts.master')
@section('content')
    <table class="table table table-striped">
        <thead>
        <tr>
            <th>Product</th>
            <th>Image</th>
            <th>Price</th>
            <th>Quantity</th>
            <th>Total</th>
            <th></th>
        </tr>
        </thead>
        <tbody>
        @forelse($cartItems as $cartItem)
            <tr>
                <td>{{ $cartItem->product->name }}</td>
                <td><img src="{{ $cartItem->product->image }}" class="img-thumbnail" style="height:50px;"></td>
                <td>{{ number_format($cartItem->price, 2) }}</td>
                <td>{{ $cartItem->quantity }}</td>
                <td>{{ number_format($cartItem->total, 2) }}</td>
                <td><a href="{{ route('cart.removeProduct', $cartItem->product_id) }}" class="text-danger">X</a></td>
            </tr>
        @empty
            <tr>
                <td colspan="6" align="center">No products found.</td>
            </tr>
        @endforelse

        </tbody>
        @if($cartItems->count() > 0)
            <tfoot>
            <tr>
                <th colspan="3"></th>
                <th>Grand Total</th>
                <th>{{ number_format($cartItems->sum('total'), 2) }}</th>
                <th></th>
            </tr>
            </tfoot>
        @endif
    </table>

    @if($cartItems->count() > 0)
        <button class="btn btn-primary btn-block" id="checkout-button"><i class="fa fa-cc-stripe"></i> Pay {{ number_format($cartItems->sum('total'), 2) }}</button>
    @endif

@endsection

@push('header')
    <script src="https://polyfill.io/v3/polyfill.min.js?version=3.52.1&features=fetch"></script>
    <script src="https://js.stripe.com/v3/"></script>
@endpush

@push('footer')
    <script type="text/javascript">
        var stripe = Stripe("{{ env('STRIPE_KEY') }}");
        var checkoutButton = document.getElementById("checkout-button");
        checkoutButton.addEventListener("click", function () {
            fetch("{{ route('checkout') }}", {
                method: "POST",
            })
            .then(function (response) {
                return response.json();
            })
            .then(function (session) {
                return stripe.redirectToCheckout({ sessionId: session.id });
            })
            .then(function (result) {
                if (result.error) {
                    alert(result.error.message);
                }
            })
            .catch(function (error) {
                console.error("Error:", error);
            });
        });
    </script>
@endpush

=> Creating Response Pages

Further, the stripe requires a return URL for the success and cancel the process. We already have routes for that. In short, let’s create success.blade.php and cancel.blade.php in resources > views folder.

/* resources > views > success.blade.php */

@extends('layouts.master')
@section('content')
    <div class="row">
        <div class="col-md-12">
            <div class="card ">
                <div class="card-header bg-success text-white">
                    <i class="fa fa-check"></i> Success
                </div>
                <div class="card-body">
                    Your order has been placed successfully. <a href="{{ route('products') }}"><i class="fa fa-plus"></i> Go to products</a>
                </div>
            </div>
        </div>
    </div>
@endsection
/* resources > views > cancel.blade.php */

@extends('layouts.master')
@section('content')
    <div class="row">
        <div class="col-md-12">
            <div class="card ">
                <div class="card-header bg-warning">
                    <i class="fa fa-exclamation-circle"></i> Cancelled
                </div>
                <div class="card-body">
                    You have cancelled your checkout. <a href="{{ route('products') }}"><i class="fa fa-plus"></i> add new products</a>
                </div>
            </div>
        </div>
    </div>
@endsection

=> Accepting Payment

Finally, open the CheckoutController and create the checkout method. As you can see, you need to set SECRET_KEY using the setApiKey method. After that, there is a session with the line_items array. I’ve created a separate lineItems method to clean up code. I’ve commented static array for clarification. You need to multiply every product price by 100.

Above all, there are two methods available, called success and cancel. I’m clearing the CartItem model after the success route is called. Both methods return specific success and cancel pages.

/* app > Http > Controllers > CheckoutController.php */

<?php

namespace App\Http\Controllers;

use App\Models\CartItem;
use Illuminate\Http\Request;
use Stripe\Checkout\Session;
use Stripe\Stripe;

class CheckoutController extends Controller
{
    public function checkout()
    {
        header('Content-Type: application/json');
        Stripe::setApiKey(env('STRIPE_SECRET'));

        $checkout_session = Session::create([
            'payment_method_types' => ['card'],
            'line_items' => [
//                STATIC ARRAY FOR DEMO
//                'price_data' => [
//                    'currency' => 'inr',
//                    'unit_amount' => $price * 100,
//                    'product_data' => [
//                        'name' => 'Static Product',
//                        'images' => ["https://placehold.it/350x250"],
//                    ],
//                ],
//                'quantity' => 1,
                $this->lineItems()
            ],
            'mode' => 'payment',
            'success_url' => 'http://localhost:8000/success',
            'cancel_url' => 'http://localhost:8000/cancel',
        ]);

        //returns session id
        return response()->json(['id' => $checkout_session->id]);
    }

    private function lineItems()
    {
        $cartItems = CartItem::all();
        $lineItems = [];
        foreach ($cartItems as $cartItem) {
            $product['price_data'] = [
                'currency' => 'INR',
                'unit_amount' => $cartItem->price * 100,
                'product_data' => [
                    'name' => $cartItem->product->name,
                    'images' => [$cartItem->product->image],
                ],
            ];
            $product['quantity'] = $cartItem->quantity;
            $lineItems[] = $product;
        }

        return $lineItems;
    }

    public function success()
    {
        CartItem::truncate();
        return view('success');
    }

    public function cancel()
    {
        return view('cancel');
    }

}

To sum up, click on the Pay button on the cart page. You will redirect to the stripe checkout page. You can check dummy card details below for processing. Also, you can visit here for stripe testing card numbers.

Stripe Implementation Checkout Page
  • Card Number: 4242 4242 4242 4242
  • Card Expiry Date: Any future date
  • Card CVV Number: Any 3 digits

Finally, the stripe will redirect you to your success page after payment confirmation.

Stripe Implementation Success Page

That’s all for laravel stripe payment gateway. I hope you learn from this. Have a nice day 🙂

Learn more about Laravel Framework

LEAVE A REPLY

Please enter your comment!
Please enter your name here