This tutorial gives you code for a Laravel eCommerce project with a “Checkout” feature. In an earlier article, we saw how to create a Laravel shopping cart with a product gallery and add-to-cart step.
This example enables the “Checkout” action for placing an order for the cart items. It follows the below steps while making the order.
In this confirmation step, we can have a mail-sending flow to notify the customer and the store admin.
ProductController
to handle the product fetch and the shop action request.OrderController
to handle the order and order-items insert request.This step creates a table structure to migrate and seeders to load initial data.
This example uses a database to have the store products and the placed order. So, the first step is to create a database and configure it with the Laravel eCommerce .env
configuration.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel-ecommerce-project
DB_USERNAME=root
DB_PASSWORD=
Run the below commands to create the PHP migration files for your Laravel eCommerce App. The migration file can be found in the Laravel project’s database/migrations
directory.
php artisan make:migration create_products_table
php artisan make:migration create_tbl_order_table
php artisan make:migration create_tbl_order_items_table
After creating the migration files, the next step is to add more fields for the table instance. The migration file has an auto-generation field and a timestamp field by default. Other fields have to be added by modifying the table instance.
The up()
function of this Migration class sends the create request to migrate. This function specifies the database table name and fields with its datatypes and length. The down()
function is for dropping the table if it exists already.
create_products_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name', 255);
$table->text('description');
$table->string('photo');
$table->decimal('price', 10, 2);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('products');
}
};
create_tbl_order.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tbl_order', function (Blueprint $table) {
$table->id();
$table->string('order_ref_id');
$table->timestamp('order_at')->nullable();
$table->string('name');
$table->string('email');
$table->text('address');
$table->decimal('order_amount', 10, 2)->default(0.00);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('tbl_order');
}
};
create_tbl_order_items.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tbl_order_items', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('order_id');
$table->unsignedBigInteger('product_id');
$table->integer('quantity');
$table->decimal('subtotal', 8, 2);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('tbl_order_items');
}
};
The tables will be moved to the database by running the artisan migrate command.
php artisan migrate
This command creates the ProductsTableSeeder
with the number DB::insert
request. Each request has its array of product row data.
php artisan make:seeder ProductsTableSeeder
In the below code, the run()
method has the DB::table('products')->insert([...])
statements to push the number of initial data into the ‘products’ table.
This seeder file is intended for Laravel’s database seeding. Seeing helps developers to load test data to the database.
ProductsTableSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class ProductsTableSeeder extends Seeder
{
public function run()
{
DB::table('products')->insert([
'name' => 'Android smartphone with a 6.5',
'description' => 'Android smartphone with a 6.5-inch display, octa-core processor, 4GB of RAM, 64GB storage (expandable), a triple rear camera setup (13MP main, 2MP depth, 2MP macro), an approximate 8MP front camera.',
'photo' => 'http://example.com/storage/app/public/products/oppo.jpg',
'price' => 698.88
]);
DB::table('products')->insert([
'name' => 'Digital Camera EOS',
'description' => 'Canon cameras come in various models with diverse features, but generally, they offer high-quality imaging, a range of resolutions, interchangeable lenses, advanced autofocus systems.',
'photo' => 'http://example.com/storage/app/public/products/canon.jpg',
'price' => 983.00
]);
DB::table('products')->insert([
'name' => 'LOIS CARON Watch',
'description' => 'The Lois Caron watch typically features a stainless steel case, quartz movement, analog display, synthetic leather or metal strap, and water resistance at varying depths.',
'photo' => 'http://example.com/storage/app/public/products/watch.jpg',
'price' => 675.00
]);
DB::table('products')->insert([
'name' => 'Elegante unisex adult square',
'description' => 'Sunglasses come in a wide variety of styles, but they generally feature UV-protective lenses housed in plastic or metal frames.',
'photo' => 'http://example.com/storage/app/public/products/sunclass.jpg',
'price' => 159.99
]);
DB::table('products')->insert([
'name' => 'Large Capacity Folding Bag',
'description' => ' A typical travel bag is designed with durable materials, multiple compartments, sturdy handles, and often includes wheels for easy maneuverability.',
'photo' => 'http://example.com/storage/app/public/products/bag.jpg',
'price' => 68.00
]);
DB::table('products')->insert([
'name' => 'Lenovo Smartchoice Ideapad 3',
'description' => 'Lenovo laptops typically offer various configurations with features such as Intel or AMD processors.',
'photo' => 'http://example.com/storage/app/public/products/laptop.jpg',
'price' => 129.99
]);
}
}
After creating the database seeder, the next step is to run the Laravel php artisan db:seed
command to populate the data in the insert statement.
php artisan db:seed --class=ProductsTableSeeder
Thus, we have done step 1 to prepare the database environment and connect it to the Laravel eCommerce application.
These routes map the handlers defined for this example to handle the customer request via the Laravel app interface. Some of them are listed below.
Each rule points to a corresponding logical control structure defined in the Laravel controller.
web.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductsController;
use App\Http\Controllers\OrderController;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
// In your routes/web.php
Route::get('/', [ProductsController::class, 'showProducts']);
Route::get('cart', [ProductsController::class, 'showCartTable']);
Route::get('checkout', [ProductsController::class, 'proceed']);
Route::get('/cart-count', [ProductsController::class, 'cartCount'])->name('cart.count');
Route::get('add-to-cart/{id}', [ProductsController::class, 'addToCart']);
Route::patch('update-cart', [ProductsController::class, 'updateCart']);
Route::delete('remove-from-cart', [ProductsController::class, 'removeCartItem']);
Route::get('clear-cart', [ProductsController::class, 'clearCart']);
Route::post('/order', [OrderController::class, 'confirmOrder'])->name('order.confirm');
Route::get('/thank-you', [ProductsController::class, 'index'])->name('thank-you');
This PHP code is created as a ProductsController
in this eCommerce project. It has the following functions.
showCartTable()
displays a cart page with a tabular view of cart session items and a product gallery.cartCount()
displays the cart item count at the eCommerce project’s page header.addToCart
, updateCart
, removeCartItem
– These functions perform cart actions add, edit, and delete for a specific product ID.clearCart()
: Clears the entire cart.Apart from the above, this controller has functions to “Proceed checkout” and show “Order receipt” on a thank-you page.
ProductsController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Product;
use App\Models\OrderItem;
class ProductsController extends Controller
{
public function showCartTable()
{
$products = Product::all();
return view('cart', compact('products'));
}
public function cartCount()
{
$cart = session()->get('cart', []);
$count = count($cart);
return response()->json(['count' => $count]);
}
public function addToCart($id)
{
$product = Product::find($id);
if (!$product) {
abort(404);
}
$cart = session()->get('cart', []);
if (!$cart) {
$cart = [
$id => [
"name" => $product->name,
"quantity" => 1,
"price" => $product->price,
"photo" => $product->photo
]
];
session()->put('cart', $cart);
return redirect()->back()->with('success', 'Product added to cart successfully!');
}
if (isset($cart[$id])) {
$cart[$id]['quantity']++;
session()->put('cart', $cart);
return redirect()->back()->with('success', 'Product added to cart successfully!');
}
$cart[$id] = [
"name" => $product->name,
"quantity" => 1,
"price" => $product->price,
"photo" => $product->photo
];
session()->put('cart', $cart);
if (request()->wantsJson()) {
return response()->json(['message' => 'Product added to cart successfully!']);
}
return redirect()->back()->with('success', 'Product added to cart successfully!');
}
public function updateCart(Request $request)
{
$id = $request->id;
$newQuantity = $request->quantity;
$cart = session()->get('cart');
if ($cart && isset($cart[$id])) {
$cart[$id]['quantity'] = $newQuantity;
session()->put('cart', $cart);
$details = $cart[$id];
$subtotal = $details['price'] * $newQuantity;
$total = collect($cart)->sum(function ($item) {
return $item['price'] * $item['quantity'];
});
return response()->json(['success' => true, 'subtotal' => $subtotal, 'total' => $total]);
}
return response()->json(['success' => false]);
}
public function removeCartItem(Request $request)
{
if ($request->id) {
$cart = session()->get('cart');
if (isset($cart[$request->id])) {
unset($cart[$request->id]);
session()->put('cart', $cart);
}
session()->flash('success', 'Product removed successfully');
}
}
public function clearCart()
{
session()->forget('cart');
return redirect()->back();
}
public function showProducts()
{
$products = Product::all();
return view('welcome', compact('products'));
}
public function proceed()
{
$products = Product::all();
return view('checkout', compact('products'));
}
public function index(Request $request)
{
$orderRefId = $request->input('orderRefId'); // Get the orderRefId from the request
return view('thankyou', compact('orderRefId'));
}
}
An OrderController
handles the confirmation of an order after collecting necessary customer details. The ordered items’ product ID, quantity, and unit price are stored in the database.
The generateRandomString()
function creates a character random reference ID for each order.
Once the order confirmation is done, the application clears the cart. Each order completion will end with a receipt page showing a ‘thank you’ message with a reference to the placed order.
OrderController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
class OrderController extends Controller
{
public function generateRandomString($length = 10)
{
return rand(100000, 999999);
}
public function confirmOrder(Request $request)
{
$request->validate([
'name' => 'required|string',
'email' => 'required|email',
'address' => 'required|string',
'product_ids' => 'required|array',
'quantity' => 'required|array',
]);
$productIds = $request->input('product_ids');
$quantities = $request->input('quantity');
$total = 0;
$orderDetails = [];
$orderRefId = $this->generateRandomString(8);
foreach ($productIds as $key => $productId) {
$product = Product::find($productId);
if ($product) {
$subtotal = $product->price * $quantities[$key];
$total += $subtotal;
$orderDetails[] = [
'order_id' => $orderRefId,
'product_id' => $productId,
'quantity' => $quantities[$key],
'subtotal' => $subtotal,
];
}
}
$order = new Order([
'order_ref_id' => $orderRefId,
'order_at' => now(),
'name' => $request->input('name'),
'email' => $request->input('email'),
'address' => $request->input('address'),
'order_amount' => $total,
]);
$order->save();
$order->orderItems()->createMany($orderDetails);
$orderDetails['total'] = $total;
session()->forget('cart');
return redirect()->route('thank-you', ['orderRefId' => $orderRefId]);
}
}
This Laravel eCommerce project has three templates. The product gallery is designed with the Laravel welcome template.
The code contains an HTML structure for a product gallery tile in a loop that iterates the $products
result. For each product in the array, it generates a card layout to display the tile.
welcome.blade.php
@section('content')
<div class="container mt-5">
<h2>Product Gallery</h2>
</div>
<div class="container products">
<div class="row">
@if(!empty($products)) @foreach($products as $product)
<div class="col-xs-12 col-sm-6 col-md-4">
<div class="card mb-4">
<img
src="{{ $product->photo }}"
class="card-img-top img-size"
alt="{{ $product->name }}"
/>
<div class="card-body">
<h5 class="card-title">{{ $product->name }}</h5>
<p class="card-text">
{{ \Illuminate\Support\Str::limit($product->description, 50) }}
</p>
<p class="card-text">
<strong>Price: </strong> ${{ $product->price }}
</p>
<a
href="javascript:void(0)"
data-product-id="{{ $product->id }}"
id="add-cart-btn-{{ $product->id }}"
class="btn btn-warning btn-block text-center add-cart-btn add-to-cart-button"
>Add to cart</a
>
<span
id="adding-cart-{{ $product->id }}"
class="btn btn-warning btn-block text-center added-msg"
style="display: none"
>Added.</span
>
</div>
</div>
</div>
@endforeach @endif
</div>
</div>
@endsection
Inside the table body, it iterates through the items stored in the cart session (session('cart'))
using @foreach
This template is used to combine the display of the user’s shopping cart items in a table format with the option to continue shopping or proceed to checkout..
cart.blade.php
@section('content')
<table id="cart" class="table table-bordered table-hover table-condensed mt-3">
<thead>
<tr>
<th style="width:50%">Product</th>
<th style="width:8%" class="text-center">Price</th>
<th style="width:8%">Quantity</th>
<th style="width:22%" class="text-center">Subtotal</th>
</tr>
</thead>
<tbody>
<?php $total = 0 ?>
@if(session('cart'))
@foreach(session('cart') as $id => $details)
<?php $total += $details['price'] * $details['quantity'] ?>
<tr>
<td data-th="Product">
<div class="row">
<div class="col-sm-3 hidden-xs"><img src="{{ $details['photo'] }}" width="50" height="" class="img-responsive" />
</div>
<div class="col-sm-9">
<p class="nomargin">{{ $details['name'] }}</p>
<p class="remove-from-cart cart-delete" data-id="{{ $id }}" title="Delete">Remove</p>
</div>
</div>
</td>
<td data-th="Subtotal" class="text-center">{{ $details['price'] }}</td>
<td data-th="Quantity">
<input type="number" value="{{ $details['quantity'] }}" class="form-control quantity" />
</td>
<td data-th="Subtotal" class="text-center">${{ number_format($details['price'] * $details['quantity'], 2) }}</td>
</tr>
@endforeach
@endif
</tbody>
<tfoot>
@if(!empty($details))
<tr class="visible-xs">
<td class="text-right" colspan="3"><strong>Total</strong></td>
<td class="text-center">${{ number_format($total, 2) }}</td>
</tr>
@else
<tr>
<td class="text-center" colspan="4">Your Cart is Empty.....</td>
<tr>
@endif
</tfoot>
</table>
<a href="{{ url('http://127.0.0.1:8000/') }}" class="btn shopping-btn">Continue Shopping</a>
<a href="{{ url('checkout') }}" class="btn btn-warning check-btn">Proceed Checkout</a>
<div class="container products">
<div class="row">
@foreach($products as $product)
<div class="col-xs-12 col-sm-6 col-md-4">
<div class="card mb-4">
<img src="{{ $product->photo }}" class="card-img-top img-size" alt="{{ $product->name }}">
<div class="card-body">
<h5 class="card-title">{{ $product->name }}</h5>
<p class="card-text">{{ \Illuminate\Support\Str::limit($product->description, 50) }}
</p>
<p class="card-text"><strong>Price: </strong> ${{ $product->price }}</p>
<a href="{{ url('add-to-cart/'.$product->id) }}" class="btn btn-warning btn-block text-center" role="button">Add to cart</a>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endsection
This checkout page template has an HTML table displaying a cart table. Each row shows the product name, price, and quantity data. The quantity is editable based on which the row subtotal will be updated on the UI.
checkout.blade.php
@section('content')
@php
$cartItems = session('cart') ?? [];
$cartNotEmpty = count($cartItems) > 0;
$total = 0;
@endphp
<form action="{{ route('order.confirm') }}" method="post">
@csrf
<table id="cart" class="table table-bordered table-hover table-condensed mt-3">
<thead>
<tr>
<th style="width:50%">Product</th>
<th style="width:8%" class="text-center">Price</th>
<th style="width:8%">Quantity</th>
<th style="width:22%" class="text-center">Subtotal</th>
</tr>
</thead>
<tbody>
@forelse($cartItems as $id => $details)
@if(is_array($details))
<tr>
<td data-th="Product">
<div class="row">
<div class="col-sm-3 hidden-xs">
<img src="{{ $details['photo'] }}" width="50" height="" class="img-responsive" />
</div>
<div class="col-sm-9">
<p class="nomargin">{{ $details['name'] }}</p>
</div>
</div>
</td>
<td data-th="Price" class="text-center">{{ $details['price'] }}</td>
<td data-th="Quantity">
<input type="number" value="{{ $details['quantity'] }}" name="quantity[]" id="quantity" class="form-control quantity" />
<input type="hidden" name="product_ids[]" value="{{ $id }}">
</td>
<td data-th="Subtotal" class="text-center">${{ number_format($details['price'] * $details['quantity'], 2) }}</td>
</tr>
<?php $total += $details['price'] * $details['quantity']; ?>
@endif
@empty
@if(!$cartNotEmpty)
<tr>
<td class="text-center" colspan="4">Your Cart is Empty.....</td>
</tr>
@endif
@endforelse
</tbody>
<tfoot>
@if($total > 0)
<tr class="visible-xs">
<td class="text-right" colspan="3"><strong>Total</strong></td>
<td class="text-center"> ${{ number_format($total, 2) }}</td>
</tr>
@endif
</tfoot>
</table>
<div class="customer-details">
<h2> Customer Details </h2>
<div class="form-grp">
<label for="">Name :</label><span class="error-message" id="name-error"></span><br>
<input type="text" class="input-field" name="name" id="fname">
</div>
<div class="form-grp">
<label for="">Email :</label><span class="error-message" id="email-error"></span><br>
<input type="text" class="input-field" name="email" id="email">
</div>
<div class="form-grp">
<label for="">Address :</label><span class="error-message" id="address-error"></span><br>
<textarea name="address" class="input-field" id="address" cols="30" rows=""></textarea>
</div>
</div>
<a href="{{ url('cart') }}" class="btn shopping-btn">Continue Shopping</a>
<input type="submit" class="btn btn-warning check-btn" value="Confirm Order">
</form>
@endsection
Clicking “Add to cart” from the gallery passes the product ID of the clicked element via AJAX.
The AJAX endpoint /add-to-cart/{productId} adds the chosen product to the session array. On the success callback, it updates the cart table and item count on the UI.
$(document).ready(function () {
$('.add-to-cart-button').on('click', function () {
var productId = $(this).data('product-id');
$.ajax({
type: 'GET',
url: '/add-to-cart/' + productId,
success: function (data) {
$("#adding-cart-" + productId).show();
$("#add-cart-btn-" + productId).hide();
var currentCount = parseInt($('.cart-count').text());
$('.cart-count').text(currentCount);
},
error: function (error) {
console.error('Error adding to cart:', error);
}
});
});
});
The cart table has a “Remove” link on each row. Clicking this link will send the below AJAX request to remove the cart item.
The script below uses a JavaScript confirmation dialog to get customer consent before deleting.
On changing the quantity input, the script below changes the sub-total accordingly.
<script type="text/javascript">
$(".remove-from-cart").click(function(e) {
e.preventDefault();
var ele = $(this);
if (confirm("Are you sure want to remove product from the cart?")) {
$.ajax({
url: '{{ url('
remove - from - cart ') }}',
method: "DELETE",
data: {
_token: '{{ csrf_token() }}',
id: ele.attr("data-id")
},
success: function(response) {
window.location.reload();
}
});
}
});
$(document).ready(function() {
$(".quantity").on('input', function() {
var newQuantity = $(this).val();
var row = $(this).closest('tr');
var id = row.find('.cart-delete').data('id');
$.ajax({
url: '{{ url('
update - cart ') }}',
method: "PATCH",
data: {
_token: '{{ csrf_token() }}',
id: id,
quantity: newQuantity
},
success: function(response) {
if (response.success) {
row.find('td[data-th="Subtotal"]').text('$' + response.subtotal);
$('.visible-xs').find('.text-center').text('$' + response.total);
} else {
alert('Failed to update quantity in the cart.');
}
},
error: function() {
alert('Error occurred while updating quantity.');
}
});
});
});
Thus, we have created a simple eCommerce project in Laravel without using any third-party library. The actions enabled in this example help to learn a basic functional dynamic shopping cart.