How to create a Laravel eCommerce project without using a package

by Vincy. Last modified on February 11th, 2024.

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.

View demo

This example enables the “Checkout” action for placing an order for the cart items. It follows the below steps while making the order.

  1. A product gallery has an “Add to Cart” button to move the products to the cart session.
  2. A shopping cart HTML table records the list of added cart items from the session.
  3. The cart table view has a control to proceed to checkout the order.
  4. A confirmation page lists the ordered item with an HTML form to collect the customer contact details.
  5. The order will be stored in the database upon the customer’s confirmation.

In this confirmation step, we can have a mail-sending flow to notify the customer and the store admin.

laravel ecommerce project

Laravel eCommerce project to-do list

  1. Create a database and tables and import initial data.
    • Create PHP migration files for the products and order database tables.
    • Create a PHP seeder to supply product data to the database to display the product gallery initially.
  2. Build eCommerce App URL rules.
  3. Bring eCommerce project’s (product and order) entities to the Laravel architectural context.
    • Create ProductController to handle the product fetch and the shop action request.
    • Create OrderController to handle the order and order-items insert request.
  4. Design the product gallery, cart, and checkout Template files.

1. Create a database and tables and import initial data

This step creates a table structure to migrate and seeders to load initial data.

Create PHP migration files for the products and order database tables

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

2. Create a Laravel PHP seeder file to load initial product data

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.

Build eCommerce App URL rules

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.

  • Product fetch to show dynamic tiles in a gallery.
  • Showing cart table from the PHP session and performing an update, delete, clear, and checkout cart actions.
  • Showing order confirmation and collecting customer details.
  • Showing acknowledgment after placing an order.

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');

eCommerce product and order entities in Laravel architectural context

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'));
    }
}

Confirm order with customer details on Laravel cart checkout

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]);
    }
}

Product gallery, cart, and checkout template design

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

Creating a checkout page template

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

Add or remove cart items from the session via AJAX

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.

View demo Download

Vincy
Written by Vincy, a web developer with 15+ years of experience and a Masters degree in Computer Science. She specializes in building modern, lightweight websites using PHP, JavaScript, React, and related technologies. Phppot helps you in mastering web development through over a decade of publishing quality tutorials.

Leave a Reply

Your email address will not be published. Required fields are marked *

↑ Back to Top

Share this page