How to Build a Shopping Cart with Laravel and Stripe

In this tutorial, we will learn how to build a shopping cart using Laravel, a popular PHP framework, and Stripe, a powerful payment gateway. By the end of this tutorial, you will have a fully functional shopping cart where users can add products, update quantities, and checkout using Stripe for secure payments.

Prerequisites

To follow along with this tutorial, you will need:

  • Basic knowledge of Laravel and PHP
  • Composer installed on your computer
  • An active Stripe account

Setting Up the Laravel Project

Let’s start by setting up a new Laravel project. Open your terminal and run the following command:

composer create-project laravel/laravel shopping-cart

This will create a new Laravel project in a folder named shopping-cart.

Next, navigate to the project folder:

cd shopping-cart

Setting Up the Database

Our shopping cart will require a database to store products, orders, and other related data. Open the .env file in the project root directory and update the database connection settings to match your development environment:

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

Create a new MySQL database named shopping_cart in your local development environment.

Generating Migrations

We will use Laravel’s migration feature to create the necessary database tables. Run the following command in your terminal to generate the migration files:

php artisan make:migration create_products_table
php artisan make:migration create_orders_table
php artisan make:migration create_order_items_table

This will generate three migration files in the database/migrations directory. Open each file and define the table schema as follows.

create_products_table.php

use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;

class CreateProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('description');
            $table->decimal('price', 8, 2);
            $table->timestamps();
        });
    }

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

create_orders_table.php

use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;

class CreateOrdersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id');
            $table->decimal('total', 8, 2);
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }

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

create_order_items_table.php

use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;

class CreateOrderItemsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('order_items', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('order_id');
            $table->unsignedBigInteger('product_id');
            $table->integer('quantity');
            $table->decimal('price', 8, 2);
            $table->timestamps();

            $table->foreign('order_id')->references('id')->on('orders')->onDelete('cascade');
            $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
        });
    }

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

Running Migrations

Now that we have defined the table schemas, let’s run the migrations to create the database tables. Run the following command in your terminal:

php artisan migrate

You should see output similar to the following:

Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
Migrating: 2021_10_24_000000_create_products_table
Migrated:  2021_10_24_000000_create_products_table
Migrating: 2021_10_24_000001_create_orders_table
Migrated:  2021_10_24_000001_create_orders_table
Migrating: 2021_10_24_000002_create_order_items_table
Migrated:  2021_10_24_000002_create_order_items_table

Creating Models

We will use Laravel’s Eloquent ORM to interact with the database. Run the following commands to generate the necessary models:

php artisan make:model Product
php artisan make:model Order
php artisan make:model OrderItem

Open each model file in the app/Models directory and define the relationships and attributes as follows.

Product.php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class Product extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'description', 'price'];

    public function orderItems()
    {
        return $this->hasMany(OrderItem::class);
    }
}

Order.php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class Order extends Model
{
    use HasFactory;

    protected $fillable = ['user_id', 'total'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function items()
    {
        return $this->hasMany(OrderItem::class);
    }
}

OrderItem.php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class OrderItem extends Model
{
    use HasFactory;

    protected $fillable = ['order_id', 'product_id', 'quantity', 'price'];

    public function order()
    {
        return $this->belongsTo(Order::class);
    }

    public function product()
    {
        return $this->belongsTo(Product::class);
    }
}

Setting Up Routes and Controllers

Next, let’s create the necessary routes and controllers to handle the shopping cart functionality.

web.php

Open the routes/web.php file and add the following routes:

use AppHttpControllersCartController;
use AppHttpControllersCheckoutController;
use AppHttpControllersProductController;

Route::get('/', [ProductController::class, 'index'])->name('products.index');
Route::get('/cart', [CartController::class, 'index'])->name('cart.index');
Route::post('/cart', [CartController::class, 'add'])->name('cart.add');
Route::post('/cart/update', [CartController::class, 'update'])->name('cart.update');
Route::post('/cart/remove', [CartController::class, 'remove'])->name('cart.remove');
Route::get('/checkout', [CheckoutController::class, 'index'])->name('checkout.index');
Route::post('/checkout', [CheckoutController::class, 'store'])->name('checkout.store');

CartController.php

In the app/Http/Controllers directory, create a new file named CartController.php with the following content:

namespace AppHttpControllers;

use AppModelsProduct;
use GloudemansShoppingcartFacadesCart;
use IlluminateHttpRequest;

class CartController extends Controller
{
    public function index()
    {
        return view('cart.index');
    }

    public function add(Request $request)
    {
        $product = Product::findOrFail($request->product_id);

        Cart::add([
            'id' => $product->id,
            'name' => $product->name,
            'price' => $product->price,
            'quantity' => $request->quantity,
        ]);

        return redirect()->route('cart.index')->with('success', 'Product added to cart successfully!');
    }

    public function update(Request $request)
    {
        Cart::update($request->rowId, $request->quantity);

        return redirect()->route('cart.index')->with('success', 'Cart updated successfully!');
    }

    public function remove(Request $request)
    {
        Cart::remove($request->rowId);

        return redirect()->route('cart.index')->with('success', 'Product removed from cart successfully!');
    }
}

CheckoutController.php

In the app/Http/Controllers directory, create a new file named CheckoutController.php with the following content:

namespace AppHttpControllers;

use AppModelsOrder;
use AppModelsOrderItem;
use IlluminateHttpRequest;
use IlluminateSupportFacadesAuth;
use StripeCharge;
use StripeCustomer;
use StripeStripe;

class CheckoutController extends Controller
{
    public function index()
    {
        return view('checkout.index');
    }

    public function store(Request $request)
    {
        // Set your Stripe API key
        Stripe::setApiKey(env('STRIPE_SECRET_KEY'));

        $customer = Customer::create([
            'email' => $request->email,
            'source' => $request->stripeToken,
        ]);

        $charge = Charge::create([
            'customer' => $customer->id,
            'amount' => Cart::subtotal() * 100,
            'currency' => 'usd',
        ]);

        $order = Order::create([
            'user_id' => Auth::id(),
            'total' => Cart::subtotal(),
        ]);

        foreach (Cart::content() as $item) {
            OrderItem::create([
                'order_id' => $order->id,
                'product_id' => $item->id,
                'quantity' => $item->qty,
                'price' => $item->price,
            ]);
        }

        Cart::destroy();

        return redirect()->route('products.index')->with('success', 'Order placed successfully!');
    }
}

Creating Views

Now let’s create the necessary views to render the cart, checkout, and product pages.

cart/index.blade.php

Create a new file named index.blade.php in the resources/views/cart directory with the following content:

@extends('layouts.app')

@section('content')
    <div class="container">
        @if (session('success'))
            <div class="alert alert-success">{{ session('success') }}</div>
        @endif

        <h2>Shopping Cart</h2>

        <table class="table table-striped">
            <thead>
                <tr>
                    <th>Product</th>
                    <th>Quantity</th>
                    <th>Price</th>
                    <th>Total</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                @foreach (Cart::content() as $item)
                    <tr>
                        <td>{{ $item->name }}</td>
                        <td>
                            <form action="{{ route('cart.update') }}" method="POST" class="d-inline-block">
                                @csrf
                                <input type="hidden" name="rowId" value="{{ $item->rowId }}">
                                <input type="number" name="quantity" value="{{ $item->qty }}">
                                <button type="submit" class="btn btn-link btn-sm">Update</button>
                            </form>
                            <form action="{{ route('cart.remove') }}" method="POST" class="d-inline-block">
                                @csrf
                                <input type="hidden" name="rowId" value="{{ $item->rowId }}">
                                <button type="submit" class="btn btn-link btn-sm">Remove</button>
                            </form>
                        </td>
                        <td>{{ $item->price }}</td>
                        <td>{{ $item->subtotal }}</td>
                        <td></td>
                    </tr>
                @endforeach
            </tbody>
        </table>
    </div>
@endsection

checkout/index.blade.php

Create a new file named index.blade.php in the resources/views/checkout directory with the following content:

@extends('layouts.app')

@section('content')
    <div class="container">
        <h2>Checkout</h2>

        <form action="{{ route('checkout.store') }}" method="POST" id="payment-form">
            @csrf

            <div class="form-group">
                <label for="email">Email</label>
                <input type="email" name="email" id="email" class="form-control" required>
            </div>

            <div id="card-element" class="form-group">
                <!-- Stripe Elements Placeholder -->
            </div>

            <button type="submit" class="btn btn-primary">Place Order</button>
        </form>
    </div>

    <script src="https://js.stripe.com/v3/"></script>
    <script>
        var stripe = Stripe('{{ env('STRIPE_PUBLIC_KEY') }}');
        var elements = stripe.elements();

        var style = {
            base: {
                color: '#32325d',
                fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
                fontSmoothing: 'antialiased',
                fontSize: '16px',
                '::placeholder': {
                    color: '#aab7c4'
                }
            },
            invalid: {
                color: '#fa755a',
                iconColor: '#fa755a'
            }
        };

        var cardElement = elements.create('card', { style: style });
        cardElement.mount('#card-element');

        var cardForm = document.getElementById('payment-form');
        cardForm.addEventListener('submit', function (event) {
            event.preventDefault();

            stripe.createToken(cardElement).then(function (result) {
                if (result.error) {
                    var errorElement = document.getElementById('card-errors');
                    errorElement.textContent = result.error.message;
                } else {
                    stripeTokenHandler(result.token);
                }
            });
        });

        function stripeTokenHandler(token) {
            var form = document.getElementById('payment-form');
            var hiddenInput = document.createElement('input');
            hiddenInput.setAttribute('type', 'hidden');
            hiddenInput.setAttribute('name', 'stripeToken');
            hiddenInput.setAttribute('value', token.id);
            form.appendChild(hiddenInput);

            form.submit();
        }
    </script>
@endsection

layouts/app.blade.php

Open the resources/views/layouts/app.blade.php file and update the content as follows:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Shopping Cart</title>
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container">
            <a class="navbar-brand" href="{{ route('products.index') }}">Shopping Cart</a>
            <ul class="navbar-nav ml-auto">
                <li class="nav-item">
                    <a class="nav-link" href="{{ route('cart.index') }}">Cart ({{ Cart::count() }})</a>
                </li>
            </ul>
        </div>
    </nav>

    <div class="py-4">
        @yield('content')
    </div>

    <script src="{{ asset('js/app.js') }}"></script>
</body>
</html>

products/index.blade.php

Create a new file named index.blade.php in the resources/views/products directory with the following content:

@extends('layouts.app')

@section('content')
    <div class="container">
        <h2>Products</h2>

        <div class="row">
            @foreach ($products as $product)
                <div class="col-md-4 mb-4">
                    <div class="card">
                        <div class="card-body">
                            <h5 class="card-title">{{ $product->name }}</h5>
                            <p class="card-text">{{ $product->description }}</p>
                            <p class="card-text">${{ $product->price }}</p>
                            <form action="{{ route('cart.add') }}" method="POST">
                                @csrf
                                <input type="hidden" name="product_id" value="{{ $product->id }}">
                                <div class="form-group">
                                    <input type="number" name="quantity" value="1" class="form-control" min="1" required>
                                </div>
                                <button type="submit" class="btn btn-primary">Add to Cart</button>
                            </form>
                        </div>
                    </div>
                </div>
            @endforeach
        </div>
    </div>
@endsection

Creating Products

To quickly populate the database with some sample products, open the database/seeders/DatabaseSeeder.php file and update the run method as follows:

public function run()
{
    AppModelsProduct::factory(10)->create();
}

Next, run the following command to seed the database:

php artisan db:seed

Stripe Configuration

To process payments with Stripe, you need to set your Stripe API key in the .env file. Open the .env file and add the following line:

STRIPE_PUBLIC_KEY=
STRIPE_SECRET_KEY=

Wrapping Up

Congratulations! You have successfully built a shopping cart with Laravel and Stripe. You can now add products to the cart, update quantities, and checkout using Stripe for secure payments.

To test the shopping cart, start the Laravel development server by running the following command:

php artisan serve

Open your web browser and navigate to http://localhost:8000` to view the list of products. Add some products to the cart and proceed to the checkout page. Enter your email address and use the test card number4242 4242 4242 4242` with any future expiration date and CVC code. After submitting the form, you should see a success message indicating that the order was placed successfully.

Feel free to customize and enhance the shopping cart according to your project requirements. Happy coding!

Related Post