Kritim Yantra
May 04, 2025
Welcome to this comprehensive guide on setting up a simple e-commerce application using Laravel 12! Whether you're a beginner looking to dive into Laravel development or an experienced developer wanting to brush up on e-commerce fundamentals, this tutorial will walk you through every step of the process.
Laravel is one of the most popular PHP frameworks, known for its elegant syntax and powerful features. By the end of this guide, you'll have a functional e-commerce application with product listings, a shopping cart, and basic checkout functionality.
Before we begin, make sure you have the following installed on your system:
First, let's create a new Laravel project. Open your terminal and run:
composer create-project laravel/laravel laravel-ecommerce
cd laravel-ecommerce
This will create a new Laravel project in a directory called laravel-ecommerce
and navigate into it.
Laravel uses a .env
file for environment configuration. Let's set up our database connection:
.env.example
to .env
php artisan key:generate
.env
:DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_ecommerce
DB_USERNAME=root
DB_PASSWORD=
Adjust these values according to your local database setup.
Laravel makes authentication easy with its built-in scaffolding. Let's install it:
composer require laravel/ui
php artisan ui bootstrap --auth
npm install && npm run dev
This will:
Let's create the necessary database tables for our e-commerce application.
php artisan make:migration create_products_table
Open the created migration file in database/migrations
and modify it:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description');
$table->decimal('price', 8, 2);
$table->string('image')->nullable();
$table->timestamps();
});
php artisan make:migration create_categories_table
Modify the migration:
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
Add a category_id foreign key to products:
php artisan make:migration add_category_id_to_products_table
Schema::table('products', function (Blueprint $table) {
$table->foreignId('category_id')->nullable()->constrained()->onDelete('set null');
});
php artisan make:migration create_orders_table
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->decimal('total', 10, 2);
$table->string('status')->default('pending');
$table->timestamps();
});
php artisan make:migration create_order_items_table
Schema::create('order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained();
$table->foreignId('product_id')->constrained();
$table->integer('quantity');
$table->decimal('price', 8, 2);
$table->timestamps();
});
Run the migrations:
php artisan migrate
Now let's create the Eloquent models for our tables.
php artisan make:model Product
Edit app/Models/Product.php
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
protected $fillable = [
'name',
'description',
'price',
'image',
'category_id'
];
public function category()
{
return $this->belongsTo(Category::class);
}
}
php artisan make:model Category
Edit app/Models/Category.php
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
protected $fillable = ['name', 'slug'];
public function products()
{
return $this->hasMany(Product::class);
}
}
php artisan make:model Order
php artisan make:model OrderItem
Edit app/Models/Order.php
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'total',
'status'
];
public function user()
{
return $this->belongsTo(User::class);
}
public function items()
{
return $this->hasMany(OrderItem::class);
}
}
Edit app/Models/OrderItem.php
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
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);
}
}
Let's create factories to generate test data.
php artisan make:factory ProductFactory
Edit database/factories/ProductFactory.php
:
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class ProductFactory extends Factory
{
public function definition(): array
{
return [
'name' => $this->faker->words(3, true),
'description' => $this->faker->paragraph,
'price' => $this->faker->randomFloat(2, 10, 1000),
'image' => $this->faker->imageUrl(400, 300, 'products', true),
'category_id' => \App\Models\Category::factory(),
];
}
}
php artisan make:factory CategoryFactory
Edit database/factories/CategoryFactory.php
:
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class CategoryFactory extends Factory
{
public function definition(): array
{
return [
'name' => $this->faker->word,
'slug' => $this->faker->slug,
];
}
}
Create a database seeder:
php artisan make:seeder DatabaseSeeder
Edit database/seeders/DatabaseSeeder.php
:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
\App\Models\User::factory()->create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => bcrypt('password'),
]);
\App\Models\Category::factory(5)->create();
\App\Models\Product::factory(20)->create();
}
}
Run the seeder:
php artisan db:seed
Let's create controllers for our e-commerce functionality.
php artisan make:controller ProductController --resource
Edit app/Http/Controllers/ProductController.php
:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index()
{
$products = Product::with('category')->latest()->paginate(12);
return view('products.index', compact('products'));
}
public function show(Product $product)
{
return view('products.show', compact('product'));
}
}
php artisan make:controller CartController
Edit app/Http/Controllers/CartController.php
:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class CartController extends Controller
{
public function index()
{
$cartItems = session()->get('cart', []);
$total = 0;
foreach ($cartItems as $item) {
$total += $item['price'] * $item['quantity'];
}
return view('cart.index', compact('cartItems', 'total'));
}
public function add(Product $product, Request $request)
{
$cart = session()->get('cart', []);
if (isset($cart[$product->id])) {
$cart[$product->id]['quantity']++;
} else {
$cart[$product->id] = [
'product_id' => $product->id,
'name' => $product->name,
'price' => $product->price,
'quantity' => 1,
'image' => $product->image
];
}
session()->put('cart', $cart);
return redirect()->back()->with('success', 'Product added to cart!');
}
public function remove($productId)
{
$cart = session()->get('cart', []);
if (isset($cart[$productId])) {
unset($cart[$productId]);
session()->put('cart', $cart);
}
return redirect()->back()->with('success', 'Product removed from cart!');
}
public function update(Request $request, $productId)
{
$cart = session()->get('cart', []);
if (isset($cart[$productId])) {
$cart[$productId]['quantity'] = $request->quantity;
session()->put('cart', $cart);
}
return redirect()->back()->with('success', 'Cart updated!');
}
}
php artisan make:controller CheckoutController
Edit app/Http/Controllers/CheckoutController.php
:
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use App\Models\OrderItem;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class CheckoutController extends Controller
{
public function index()
{
$cartItems = session()->get('cart', []);
if (empty($cartItems)) {
return redirect()->route('products.index')->with('error', 'Your cart is empty!');
}
$total = 0;
foreach ($cartItems as $item) {
$total += $item['price'] * $item['quantity'];
}
return view('checkout.index', compact('cartItems', 'total'));
}
public function store(Request $request)
{
$cartItems = session()->get('cart', []);
if (empty($cartItems)) {
return redirect()->route('products.index')->with('error', 'Your cart is empty!');
}
$total = 0;
foreach ($cartItems as $item) {
$total += $item['price'] * $item['quantity'];
}
$order = Order::create([
'user_id' => Auth::id(),
'total' => $total,
'status' => 'pending'
]);
foreach ($cartItems as $item) {
OrderItem::create([
'order_id' => $order->id,
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'price' => $item['price']
]);
}
session()->forget('cart');
return redirect()->route('orders.show', $order)->with('success', 'Order placed successfully!');
}
}
php artisan make:controller OrderController
Edit app/Http/Controllers/OrderController.php
:
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class OrderController extends Controller
{
public function index()
{
$orders = Auth::user()->orders()->latest()->paginate(10);
return view('orders.index', compact('orders'));
}
public function show(Order $order)
{
if ($order->user_id !== Auth::id()) {
abort(403);
}
return view('orders.show', compact('order'));
}
}
Now let's create the views for our application.
Create resources/views/layouts/app.blade.php
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Laravel E-Commerce</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
@vite(['resources/sass/app.scss', 'resources/js/app.js'])
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ route('home') }}">Laravel Shop</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ route('products.index') }}">Products</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link position-relative" href="{{ route('cart.index') }}">
Cart
@if(count(session()->get('cart', [])))
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ array_sum(array_column(session()->get('cart', []), 'quantity')) }}
</span>
@endif
</a>
</li>
@auth
<li class="nav-item">
<a class="nav-link" href="{{ route('orders.index') }}">My Orders</a>
</li>
<li class="nav-item">
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="nav-link btn btn-link">Logout</button>
</form>
</li>
@else
<li class="nav-item">
<a class="nav-link" href="{{ route('login') }}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('register') }}">Register</a>
</li>
@endauth
</ul>
</div>
</div>
</nav>
<div class="container my-4">
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
@yield('content')
</div>
<footer class="bg-dark text-white py-4 mt-5">
<div class="container text-center">
<p>© {{ date('Y') }} Laravel E-Commerce. All rights reserved.</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Create resources/views/products/index.blade.php
:
@extends('layouts.app')
@section('content')
<div class="row mb-4">
<div class="col-12">
<h1>Our Products</h1>
</div>
</div>
<div class="row">
@foreach($products as $product)
<div class="col-md-4 mb-4">
<div class="card h-100">
<img src="{{ $product->image ?? 'https://via.placeholder.com/400x300' }}" class="card-img-top" alt="{{ $product->name }}">
<div class="card-body">
<h5 class="card-title">{{ $product->name }}</h5>
<p class="card-text">{{ Str::limit($product->description, 100) }}</p>
<p class="h5">${{ number_format($product->price, 2) }}</p>
</div>
<div class="card-footer bg-white">
<a href="{{ route('products.show', $product) }}" class="btn btn-primary">View Details</a>
<form action="{{ route('cart.add', $product) }}" method="POST" class="d-inline">
@csrf
<button type="submit" class="btn btn-success">Add to Cart</button>
</form>
</div>
</div>
</div>
@endforeach
</div>
<div class="row">
<div class="col-12">
{{ $products->links() }}
</div>
</div>
@endsection
Create resources/views/products/show.blade.php
:
@extends('layouts.app')
@section('content')
<div class="row">
<div class="col-md-6">
<img src="{{ $product->image ?? 'https://via.placeholder.com/400x300' }}" class="img-fluid" alt="{{ $product->name }}">
</div>
<div class="col-md-6">
<h1>{{ $product->name }}</h1>
<p class="h3 text-primary">${{ number_format($product->price, 2) }}</p>
<p>{{ $product->description }}</p>
@if($product->category)
<p>Category: {{ $product->category->name }}</p>
@endif
<form action="{{ route('cart.add', $product) }}" method="POST" class="mt-4">
@csrf
<button type="submit" class="btn btn-primary btn-lg">Add to Cart</button>
</form>
</div>
</div>
@endsection
Create resources/views/cart/index.blade.php
:
@extends('layouts.app')
@section('content')
<div class="row mb-4">
<div class="col-12">
<h1>Shopping Cart</h1>
</div>
</div>
@if(count($cartItems) > 0)
<div class="row">
<div class="col-12">
<table class="table table-striped">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
<th>Total</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($cartItems as $item)
<tr>
<td>
<img src="{{ $item['image'] ?? 'https://via.placeholder.com/50' }}" width="50" class="me-2">
{{ $item['name'] }}
</td>
<td>${{ number_format($item['price'], 2) }}</td>
<td>
<form action="{{ route('cart.update', $item['product_id']) }}" method="POST" class="d-inline">
@csrf
<input type="number" name="quantity" value="{{ $item['quantity'] }}" min="1" class="form-control d-inline" style="width: 70px;">
</form>
</td>
<td>${{ number_format($item['price'] * $item['quantity'], 2) }}</td>
<td>
<form action="{{ route('cart.remove', $item['product_id']) }}" method="POST" class="d-inline">
@csrf
<button type="submit" class="btn btn-danger btn-sm">Remove</button>
</form>
</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-end"><strong>Total:</strong></td>
<td colspan="2">${{ number_format($total, 2) }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="row">
<div class="col-12 text-end">
<a href="{{ route('products.index') }}" class="btn btn-secondary">Continue Shopping</a>
<a href="{{ route('checkout.index') }}" class="btn btn-primary">Proceed to Checkout</a>
</div>
</div>
@else
<div class="row">
<div class="col-12">
<div class="alert alert-info">
Your cart is empty. <a href="{{ route('products.index') }}">Browse our products</a>.
</div>
</div>
</div>
@endif
@endsection
Create resources/views/checkout/index.blade.php
:
@extends('layouts.app')
@section('content')
<div class="row mb-4">
<div class="col-12">
<h1>Checkout</h1>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header">
<h4>Order Summary</h4>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
<th>Total</th>
</tr>
</thead>
<tbody>
@foreach($cartItems as $item)
<tr>
<td>{{ $item['name'] }}</td>
<td>${{ number_format($item['price'], 2) }}</td>
<td>{{ $item['quantity'] }}</td>
<td>${{ number_format($item['price'] * $item['quantity'], 2) }}</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-end"><strong>Total:</strong></td>
<td>${{ number_format($total, 2) }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h4>Payment Information</h4>
</div>
<div class="card-body">
<form action="{{ route('checkout.store') }}" method="POST">
@csrf
<div class="mb-3">
<label for="name" class="form-label">Name on Card</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="mb-3">
<label for="card" class="form-label">Card Number</label>
<input type="text" class="form-control" id="card" required>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="expiry" class="form-label">Expiry Date</label>
<input type="text" class="form-control" id="expiry" placeholder="MM/YY" required>
</div>
<div class="col-md-6">
<label for="cvv" class="form-label">CVV</label>
<input type="text" class="form-control" id="cvv" required>
</div>
</div>
<button type="submit" class="btn btn-primary w-100">Place Order</button>
</form>
</div>
</div>
</div>
</div>
@endsection
Create resources/views/orders/index.blade.php
:
@extends('layouts.app')
@section('content')
<div class="row mb-4">
<div class="col-12">
<h1>My Orders</h1>
</div>
</div>
<div class="row">
<div class="col-12">
@if($orders->count() > 0)
<table class="table table-striped">
<thead>
<tr>
<th>Order #</th>
<th>Date</th>
<th>Total</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($orders as $order)
<tr>
<td>{{ $order->id }}</td>
<td>{{ $order->created_at->format('M d, Y') }}</td>
<td>${{ number_format($order->total, 2) }}</td>
<td>
<span class="badge bg-{{ $order->status == 'completed' ? 'success' : 'warning' }}">
{{ ucfirst($order->status) }}
</span>
</td>
<td>
<a href="{{ route('orders.show', $order) }}" class="btn btn-sm btn-primary">View</a>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $orders->links() }}
@else
<div class="alert alert-info">
You haven't placed any orders yet. <a href="{{ route('products.index') }}">Browse our products</a>.
</div>
@endif
</div>
</div>
@endsection
Create resources/views/orders/show.blade.php
:
@extends('layouts.app')
@section('content')
<div class="row mb-4">
<div class="col-12">
<h1>Order #{{ $order->id }}</h1>
<p class="text-muted">Placed on {{ $order->created_at->format('F j, Y \a\t g:i a') }}</p>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4>Order Details</h4>
</div>
<div class="card-body">
<p><strong>Status:</strong>
<span class="badge bg-{{ $order->status == 'completed' ? 'success' : 'warning' }}">
{{ ucfirst($order->status) }}
</span>
</p>
<p><strong>Total:</strong> ${{ number_format($order->total, 2) }}</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4>Order Items</h4>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
<th>Total</th>
</tr>
</thead>
<tbody>
@foreach($order->items as $item)
<tr>
<td>{{ $item->product->name }}</td>
<td>${{ number_format($item->price, 2) }}</td>
<td>{{ $item->quantity }}</td>
<td>${{ number_format($item->price * $item->quantity, 2) }}</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-end"><strong>Total:</strong></td>
<td>${{ number_format($order->total, 2) }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
@endsection
Edit routes/web.php
:
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductController;
use App\Http\Controllers\CartController;
use App\Http\Controllers\CheckoutController;
use App\Http\Controllers\OrderController;
Route::get('/', function () {
return view('welcome');
})->name('home');
Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
// Products
Route::resource('products', ProductController::class)->only(['index', 'show']);
// Cart
Route::prefix('cart')->group(function () {
Route::get('/', [CartController::class, 'index'])->name('cart.index');
Route::post('/add/{product}', [CartController::class, 'add'])->name('cart.add');
Route::post('/update/{productId}', [CartController::class, 'update'])->name('cart.update');
Route::post('/remove/{productId}', [CartController::class, 'remove'])->name('cart.remove');
});
// Checkout
Route::prefix('checkout')->group(function () {
Route::get('/', [CheckoutController::class, 'index'])->name('checkout.index');
Route::post('/', [CheckoutController::class, 'store'])->name('checkout.store');
});
// Orders
Route::prefix('orders')->group(function () {
Route::get('/', [OrderController::class, 'index'])->name('orders.index');
Route::get('/{order}', [OrderController::class, 'show'])->name('orders.show');
});
Create resources/sass/app.scss
:
// Fonts
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap');
// Variables
@import 'variables';
// Bootstrap
@import 'bootstrap/scss/bootstrap';
body {
font-family: 'Open Sans', sans-serif;
}
.card {
transition: transform 0.3s ease;
&:hover {
transform: translateY(-5px);
}
}
.product-image {
height: 200px;
object-fit: contain;
}
.badge {
font-size: 0.8rem;
}
.form-control:focus {
box-shadow: none;
border-color: #86b7fe;
}
Now that everything is set up, let's test our application:
php artisan serve
http://localhost:8000
in your browserHere are some additional features you could add to make your e-commerce application more robust:
Congratulations! You've successfully built a simple e-commerce application with Laravel 12. This application includes:
This is just the beginning of what you can do with Laravel for e-commerce. From here, you can continue to expand and enhance your application with more advanced features as you become more comfortable with the framework.
Remember to always:
Happy coding with Laravel!
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google