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 number
4242 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!