Back to blog

Saturday, April 26, 2025

Laravel E-commerce Project Documentation

cover

Laravel E-commerce Project Documentation

This comprehensive guide walks through creating a Laravel e-commerce application with products, categories, user roles, and authentication. Each section covers concepts, implementation details, and practical examples.

Table of Contents

  1. Setting Up Models and Migrations
  2. Understanding Model Properties
  3. Middleware & Authentication
  4. Routing Strategies
  5. Controllers Implementation
  6. Frontend Authentication
  7. Form Handling
  8. Image Upload
  9. Slug Generation and Routing

1. Setting Up Models and Migrations

Creating Migrations

First, create the necessary migrations:

# Create migration for categories
php artisan make:migration create_categories_table

# Create migration for products
php artisan make:migration create_products_table

# Create migration to add role to users table
php artisan make:migration add_role_to_users_table

Creating Models

# Create Category model
php artisan make:model Category

# Create Product model
php artisan make:model Product

Migration Structure

The categories table migration:

Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->string('image')->nullable();
    $table->text('description')->nullable();
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

The products table migration:

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->foreignId('category_id')->constrained()->onDelete('cascade');
    $table->string('name');
    $table->string('slug')->unique();
    $table->text('description')->nullable();
    $table->decimal('price', 10, 2);
    $table->decimal('old_price', 10, 2)->nullable();
    $table->integer('stock')->default(0);
    $table->string('image')->nullable();
    $table->json('gallery')->nullable();
    $table->boolean('is_featured')->default(false);
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

Add role to users table:

Schema::table('users', function (Blueprint $table) {
    $table->enum('role', ['ADMIN', 'USER', 'VENDOR'])->default('USER')->after('email');
});

2. Understanding Model Properties

Complete Model Implementations

Category Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Category extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'slug',
        'image',
        'description',
        'is_active',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'is_active' => 'boolean',
    ];

    /**
     * Get all products for this category
     */
    public function products(): HasMany
    {
        return $this->hasMany(Product::class);
    }

    /**
     * Scope a query to only include active categories.
     */
    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }
}

Product Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Product extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'category_id',
        'name',
        'slug',
        'description',
        'price',
        'old_price',
        'stock',
        'image',
        'gallery',
        'is_featured',
        'is_active',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'price' => 'decimal:2',
        'old_price' => 'decimal:2',
        'is_featured' => 'boolean',
        'is_active' => 'boolean',
        'gallery' => 'array',
    ];

    /**
     * Get the category that owns the product
     */
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    /**
     * Scope a query to find similar products
     * (Same category, excluding the current product)
     */
    public function scopeSimilar($query, $productId)
    {
        $product = static::findOrFail($productId);

        return $query->where('category_id', $product->category_id)
                    ->where('id', '!=', $productId)
                    ->where('is_active', true);
    }

    /**
     * Scope a query to only include active products.
     */
    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    /**
     * Scope a query to only include featured products.
     */
    public function scopeFeatured($query)
    {
        return $query->where('is_featured', true);
    }

    /**
     * Scope a query to only include in-stock products.
     */
    public function scopeInStock($query)
    {
        return $query->where('stock', '>', 0);
    }
}

User Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'role',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    /**
     * Check if user is admin
     */
    public function isAdmin(): bool
    {
        return $this->role === 'ADMIN';
    }

    /**
     * Check if user is vendor
     */
    public function isVendor(): bool
    {
        return $this->role === 'VENDOR';
    }

    /**
     * Check if user has a specific role
     */
    public function hasRole(string $role): bool
    {
        return $this->role === $role;
    }
}

Understanding $fillable

The $fillable property in Laravel models is a critical security feature that defines which attributes can be mass-assigned. This is an important protection mechanism against mass assignment vulnerabilities.

protected $fillable = [
    'category_id',
    'name',
    'slug',
    'description',
    'price',
    'old_price',
    'stock',
    'image',
    'gallery',
    'is_featured',
    'is_active',
];

Purpose and Function:

  1. Mass Assignment Protection: The $fillable array specifies which attributes can be filled via mass assignment (like when using create() or update() methods with an array of values).

  2. Security: It prevents malicious users from trying to update fields they shouldn't have access to. For example, if you had an "is_admin" field but didn't want it to be mass-assignable, you would exclude it from $fillable.

When you use methods like:

Product::create($request->all());
// OR
$product->update($request->all());

Laravel will only accept and process the attributes listed in the $fillable array.

The Alternative: $guarded

As an alternative to $fillable, you could use $guarded, which works in the opposite way:

protected $guarded = ['id']; // Everything EXCEPT 'id' is mass assignable

This specifies which attributes are NOT mass assignable. Most Laravel developers prefer $fillable because it's more explicit about what can be assigned (whitelist approach) rather than what can't (blacklist approach).

Understanding $casts

The $casts property automatically converts attributes to specified types when accessed:

protected $casts = [
    'price' => 'decimal:2',
    'old_price' => 'decimal:2',
    'is_featured' => 'boolean',
    'is_active' => 'boolean',
    'gallery' => 'array',
];

Purpose:

  • It ensures data is consistently handled in the correct format
  • Converts database types to useful PHP types
  • Handles serialization/deserialization of complex data

Example use cases:

  • decimal:2 - Ensures price is always a decimal with 2 digits
  • boolean - Converts 0/1 in database to true/false in PHP
  • array - Converts JSON string to PHP array automatically

Query Scopes

Scopes encapsulate common query constraints into reusable methods:

public function scopeSimilar($query, $productId) {
    $product = static::findOrFail($productId);

    return $query->where('category_id', $product->category_id)
                ->where('id', '!=', $productId)
                ->where('is_active', true);
}

Naming Convention: Always prefix with scope followed by the name you want to use when calling it (with first letter capitalized).

Usage: When you call Product::similar($productId), Laravel removes the "scope" prefix and makes the first letter lowercase.

Benefit: Reusable query logic that's maintainable in a single location.

3. Middleware & Authentication

Creating Custom Middleware

php artisan make:middleware CheckRole

Role Middleware Implementation

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class CheckRole
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next, string $role): Response
    {
        if (!Auth::check()) {
            return redirect()->route('login');
        }

        if (Auth::user()->role !== $role) {
            return redirect()->route('dashboard')
                ->with('error', 'You do not have permission to access this page.');
        }

        return $next($request);
    }
}

Registering Middleware in Kernel.php

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    // ... other properties ...

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array<string, class-string|string>
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \App\Http\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'role' => \App\Http\Middleware\CheckRole::class, // Add this line
    ];
}

Purpose of Middleware

Middleware acts as filters for HTTP requests entering your application. Common uses include:

  1. Authentication: Verifying the user is logged in
  2. Authorization: Checking if users have permission to access a resource
  3. Data transformation: Modifying request data before it reaches controllers
  4. Session management: Handling session state

In our e-commerce app, middleware is crucial for:

  • Restricting admin dashboard to admin users only
  • Restricting vendor dashboard to vendor users only
  • Ensuring users are authenticated for protected areas

Inertia Middleware for Shared Data

<?php

namespace App\Http\Middleware;

use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    /**
     * The root template that's loaded on the first page visit.
     *
     * @see https://inertiajs.com/server-side-setup#root-template
     * @var string
     */
    protected $rootView = 'app';

    /**
     * Determines the current asset version.
     *
     * @see https://inertiajs.com/asset-versioning
     * @param  \Illuminate\Http\Request  $request
     * @return string|null
     */
    public function version(Request $request): ?string
    {
        return parent::version($request);
    }

    /**
     * Defines the props that are shared by default.
     *
     * @see https://inertiajs.com/shared-data
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            'auth' => [
                'user' => $request->user() ? [
                    'id' => $request->user()->id,
                    'name' => $request->user()->name,
                    'email' => $request->user()->email,
                    'role' => $request->user()->role,
                ] : null,
            ],
            'flash' => [
                'message' => fn () => $request->session()->get('message'),
                'success' => fn () => $request->session()->get('success'),
                'error' => fn () => $request->session()->get('error'),
            ],
        ]);
    }
}

This middleware is essential for passing data from Laravel to your React frontend. It makes the authenticated user (with role information) available in all Inertia components.

4. Routing Strategies

Basic Route Setup

php artisan make:controller Shop/CategoryController
php artisan make:controller Shop/ProductController
php artisan make:controller Admin/CategoryController --resource
php artisan make:controller Admin/ProductController --resource

Complete Web Routes Implementation

<?php

use App\Http\Controllers\Admin\CategoryController as AdminCategoryController;
use App\Http\Controllers\Admin\ProductController as AdminProductController;
use App\Http\Controllers\Shop\CategoryController as ShopCategoryController;
use App\Http\Controllers\Shop\ProductController as ShopProductController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
*/

Route::get('/', function () {
    return redirect()->route('shop.home');
});

// Shop/E-commerce Routes (Public)
Route::prefix('shop')->name('shop.')->group(function () {
    Route::get('/', [ShopProductController::class, 'featured'])->name('home');

    // Category Routes
    Route::get('/categories', [ShopCategoryController::class, 'index'])->name('categories.index');
    Route::get('/categories/{slug}', [ShopCategoryController::class, 'show'])->name('categories.show');

    // Product Routes
    Route::get('/products', [ShopProductController::class, 'index'])->name('products.index');
    Route::get('/products/{slug}', [ShopProductController::class, 'show'])->name('products.show');
    Route::get('/products/similar/{productId}', [ShopProductController::class, 'similar'])->name('products.similar');
});

// User Dashboard (Authenticated Users)
Route::middleware(['auth'])->group(function () {
    Route::get('/dashboard', function () {
        return inertia('Dashboard');
    })->name('dashboard');

    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

// Admin Routes (Admin Users Only)
Route::prefix('admin')->name('admin.')->middleware(['auth', 'role:ADMIN'])->group(function () {
    // Dashboard
    Route::get('/dashboard', [AdminProductController::class, 'dashboard'])->name('dashboard');

    // Category CRUD
    Route::resource('categories', AdminCategoryController::class);

    // Product CRUD
    Route::resource('products', AdminProductController::class);
});

// Vendor Routes (Vendor Users Only)
Route::prefix('vendor')->name('vendor.')->middleware(['auth', 'role:VENDOR'])->group(function () {
    // Vendor Dashboard
    Route::get('/dashboard', function () {
        return inertia('Vendor/Dashboard');
    })->name('dashboard');

    // Add vendor-specific routes here
});

// Auth routes are handled by Laravel Breeze/Inertia
require __DIR__.'/auth.php';

Route Groups

Route groups organize related routes and allow shared attributes:

// Shop/E-commerce Routes (Public)
Route::prefix('shop')->name('shop.')->group(function () {
    // Routes defined here will have the prefix 'shop' and name prefix 'shop.'
});

// Admin Routes (Admin Users Only)
Route::prefix('admin')->name('admin.')->middleware(['auth', 'role:ADMIN'])->group(function () {
    // Routes defined here will have admin prefix, middleware, and name prefix 'admin.'
});

Route Prefixes

A prefix adds a common base path to all routes within a group:

Route::prefix('admin')->group(function () {
    // Routes defined here will start with /admin
    Route::get('/dashboard', ...); // Results in /admin/dashboard
});

Purpose: Prefixes organize routes under a common namespace, improving code structure and URL readability.

Route Names

Route naming creates identifiable references for generating URLs:

Route::get('/products', [ProductController::class, 'index'])->name('products.index');

Usage:

// In controllers/views
return redirect()->route('products.index');
// Instead of
return redirect('/products');

Benefits:

  1. URLs can be changed without updating references throughout the codebase
  2. Route names provide a centralized "dictionary" of application paths
  3. Name prefixes (like admin. or shop.) create organized namespaces
  4. Makes using route groups more maintainable

5. Controllers Implementation

Resource Controllers

Resource controllers follow RESTful conventions with standard methods:

php artisan make:controller Admin/CategoryController --resource

This creates a controller with index, create, store, show, edit, update, and destroy methods.

Complete Controller Implementations

Admin Category Controller

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class CategoryController extends Controller
{
    /**
     * Display a listing of the categories
     */
    public function index()
    {
        $categories = Category::withCount('products')->latest()->paginate(10);

        return inertia('Admin/Categories/Index', [
            'categories' => $categories
        ]);
    }

    /**
     * Show the form for creating a new category
     */
    public function create()
    {
        return inertia('Admin/Categories/Create');
    }

    /**
     * Store a newly created category
     */
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'image' => 'nullable|image|max:2048',
            'is_active' => 'boolean',
        ]);

        // Generate slug
        $validated['slug'] = Str::slug($validated['name']);

        // Handle image upload
        if ($request->hasFile('image')) {
            $validated['image'] = $request->file('image')->store('categories', 'public');
        }

        Category::create($validated);

        return redirect()->route('admin.categories.index')
            ->with('success', 'Category created successfully');
    }

    /**
     * Show the specified category
     */
    public function show(Category $category)
    {
        $category->load('products');

        return inertia('Admin/Categories/Show', [
            'category' => $category
        ]);
    }

    /**
     * Show the form for editing the category
     */
    public function edit(Category $category)
    {
        return inertia('Admin/Categories/Edit', [
            'category' => $category
        ]);
    }

    /**
     * Update the category
     */
    public function update(Request $request, Category $category)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'image' => 'nullable|image|max:2048',
            'is_active' => 'boolean',
        ]);

        // Generate slug
        if ($validated['name'] !== $category->name) {
            $validated['slug'] = Str::slug($validated['name']);
        }

        // Handle image upload
        if ($request->hasFile('image')) {
            // Delete old image if exists
            if ($category->image) {
                Storage::disk('public')->delete($category->image);
            }

            $validated['image'] = $request->file('image')->store('categories', 'public');
        }

        $category->update($validated);

        return redirect()->route('admin.categories.index')
            ->with('success', 'Category updated successfully');
    }

    /**
     * Remove the category
     */
    public function destroy(Category $category)
    {
        // Delete image if exists
        if ($category->image) {
            Storage::disk('public')->delete($category->image);
        }

        $category->delete();

        return redirect()->route('admin.categories.index')
            ->with('success', 'Category deleted successfully');
    }
}

Admin Product Controller

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Category;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class ProductController extends Controller
{
    /**
     * Display a listing of the products
     */
    public function index()
    {
        $products = Product::with('category')->latest()->paginate(10);

        return inertia('Admin/Products/Index', [
            'products' => $products
        ]);
    }

    /**
     * Show the form for creating a new product
     */
    public function create()
    {
        $categories = Category::where('is_active', true)->get();

        return inertia('Admin/Products/Create', [
            'categories' => $categories
        ]);
    }

    /**
     * Store a newly created product
     */
    public function store(Request $request)
    {
        $validated = $request->validate([
            'category_id' => 'required|exists:categories,id',
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'price' => 'required|numeric|min:0',
            'old_price' => 'nullable|numeric|min:0',
            'stock' => 'required|integer|min:0',
            'image' => 'nullable|image|max:2048',
            'gallery' => 'nullable|array',
            'gallery.*' => 'image|max:2048',
            'is_featured' => 'boolean',
            'is_active' => 'boolean',
        ]);

        // Generate slug
        $validated['slug'] = Str::slug($validated['name']);

        // Handle main image upload
        if ($request->hasFile('image')) {
            $validated['image'] = $request->file('image')->store('products', 'public');
        }

        // Handle gallery images upload
        if ($request->hasFile('gallery')) {
            $gallery = [];

            foreach ($request->file('gallery') as $image) {
                $gallery[] = $image->store('products/gallery', 'public');
            }

            $validated['gallery'] = $gallery;
        }

        Product::create($validated);

        return redirect()->route('admin.products.index')
            ->with('success', 'Product created successfully');
    }

    /**
     * Show the specified product
     */
    public function show(Product $product)
    {
        $product->load('category');

        return inertia('Admin/Products/Show', [
            'product' => $product
        ]);
    }

    /**
     * Show the form for editing the product
     */
    public function edit(Product $product)
    {
        $categories = Category::where('is_active', true)->get();

        return inertia('Admin/Products/Edit', [
            'product' => $product,
            'categories' => $categories
        ]);
    }

    /**
     * Update the product
     */
    public function update(Request $request, Product $product)
    {
        $validated = $request->validate([
            'category_id' => 'required|exists:categories,id',
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'price' => 'required|numeric|min:0',
            'old_price' => 'nullable|numeric|min:0',
            'stock' => 'required|integer|min:0',
            'image' => 'nullable|image|max:2048',
            'gallery' => 'nullable|array',
            'gallery.*' => 'image|max:2048',
            'is_featured' => 'boolean',
            'is_active' => 'boolean',
        ]);

        // Generate slug if name changed
        if ($validated['name'] !== $product->name) {
            $validated['slug'] = Str::slug($validated['name']);
        }

        // Handle main image upload
        if ($request->hasFile('image')) {
            // Delete old image if exists
            if ($product->image) {
                Storage::disk('public')->delete($product->image);
            }

            $validated['image'] = $request->file('image')->store('products', 'public');
        }

        // Handle gallery images upload
        if ($request->hasFile('gallery')) {
            // Delete old gallery images if exist
            if ($product->gallery) {
                foreach ($product->gallery as $image) {
                    Storage::disk('public')->delete($image);
                }
            }

            $gallery = [];

            foreach ($request->file('gallery') as $image) {
                $gallery[] = $image->store('products/gallery', 'public');
            }

            $validated['gallery'] = $gallery;
        }

        $product->update($validated);

        return redirect()->route('admin.products.index')
            ->with('success', 'Product updated successfully');
    }

    /**
     * Remove the product
     */
    public function destroy(Product $product)
    {
        // Delete main image if exists
        if ($product->image) {
            Storage::disk('public')->delete($product->image);
        }

        // Delete gallery images if exist
        if ($product->gallery) {
            foreach ($product->gallery as $image) {
                Storage::disk('public')->delete($image);
            }
        }

        $product->delete();

        return redirect()->route('admin.products.index')
            ->with('success', 'Product deleted successfully');
    }

    /**
     * Get dashboard data
     */
    public function dashboard()
    {
        $totalProducts = Product::count();
        $totalCategories = Category::count();
        $featuredProducts = Product::where('is_featured', true)->count();
        $outOfStockProducts = Product::where('stock', 0)->count();

        $recentProducts = Product::with('category')
            ->latest()
            ->take(5)
            ->get();

        return inertia('Admin/Dashboard', [
            'stats' => [
                'totalProducts' => $totalProducts,
                'totalCategories' => $totalCategories,
                'featuredProducts' => $featuredProducts,
                'outOfStockProducts' => $outOfStockProducts,
            ],
            'recentProducts' => $recentProducts
        ]);
    }
}

Shop Category Controller

<?php

namespace App\Http\Controllers\Shop;

use App\Http\Controllers\Controller;
use App\Models\Product;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    /**
     * Display a listing of the products
     */
    public function index(Request $request)
    {
        $query = Product::with('category')
            ->where('is_active', true);

        // Handle search
        if ($request->has('search')) {
            $search = $request->input('search');
            $query->where(function($q) use ($search) {
                $q->where('name', 'like', "%{$search}%")
                  ->orWhere('description', 'like', "%{$search}%");
            });
        }

        // Handle category filter
        if ($request->has('category')) {
            $categoryId = $request->input('category');
            $query->where('category_id', $categoryId);
        }

        // Handle price filter
        if ($request->has('min_price') && $request->has('max_price')) {
            $minPrice = $request->input('min_price');
            $maxPrice = $request->input('max_price');
            $query->whereBetween('price', [$minPrice, $maxPrice]);
        }

        // Handle sorting
        $sort = $request->input('sort', 'latest');
        switch ($sort) {
            case 'price_low':
                $query->orderBy('price', 'asc');
                break;
            case 'price_high':
                $query->orderBy('price', 'desc');
                break;
            case 'name':
                $query->orderBy('name', 'asc');
                break;
            default:
                $query->latest();
                break;
        }

        $products = $query->paginate(12);
        $categories = Category::where('is_active', true)->get();

        return inertia('Shop/Products/Index', [
            'products' => $products,
            'categories' => $categories,
            'filters' => $request->only(['search', 'category', 'min_price', 'max_price', 'sort'])
        ]);
    }

    /**
     * Display the specified product
     */
    public function show($slug)
    {
        $product = Product::with('category')
            ->where('slug', $slug)
            ->where('is_active', true)
            ->firstOrFail();

        // Get similar products
        $similarProducts = Product::similar($product->id)
            ->take(4)
            ->get();

        return inertia('Shop/Products/Show', [
            'product' => $product,
            'similarProducts' => $similarProducts
        ]);
    }

    /**
     * Get featured products for homepage
     */
    public function featured()
    {
        $featuredProducts = Product::with('category')
            ->where('is_active', true)
            ->where('is_featured', true)
            ->take(8)
            ->get();

        return inertia('Shop/Home', [
            'featuredProducts' => $featuredProducts
        ]);
    }

    /**
     * Get similar products
     */
    public function similar($productId)
    {
        $similarProducts = Product::similar($productId)
            ->take(4)
            ->get();

        return response()->json([
            'products' => $similarProducts
        ]);
    }
}

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Product;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    /**
     * Get all active products with filtering options
     */
    public function index(Request $request)
    {
        $query = Product::with('category')
            ->where('is_active', true);

        // Handle search
        if ($request->has('search')) {
            $search = $request->input('search');
            $query->where(function($q) use ($search) {
                $q->where('name', 'like', "%{$search}%")
                  ->orWhere('description', 'like', "%{$search}%");
            });
        }

        // Handle category filter
        if ($request->has('category_id')) {
            $categoryId = $request->input('category_id');
            $query->where('category_id', $categoryId);
        }

        // Handle price filter
        if ($request->has('min_price') && $request->has('max_price')) {
            $minPrice = $request->input('min_price');
            $maxPrice = $request->input('max_price');
            $query->whereBetween('price', [$minPrice, $maxPrice]);
        }

        // Handle featured filter
        if ($request->has('featured')) {
            $featured = $request->boolean('featured');
            $query->where('is_featured', $featured);
        }

        // Handle sorting
        $sort = $request->input('sort', 'latest');
        switch ($sort) {
            case 'price_low':
                $query->orderBy('price', 'asc');
                break;
            case 'price_high':
                $query->orderBy('price', 'desc');
                break;
            case 'name':
                $query->orderBy('name', 'asc');
                break;
            default:
                $query->latest();
                break;
        }

        $perPage = $request->input('per_page', 12);
        $products = $query->paginate($perPage);

        return response()->json([
            'products' => $products
        ]);
    }

    /**
     * Get a specific product by slug
     */
    public function show($slug)
    {
        $product = Product::with('category')
            ->where('slug', $slug)
            ->where('is_active', true)
            ->firstOrFail();

        return response()->json([
            'product' => $product
        ]);
    }

    /**
     * Get similar products
     */
    public function similar($productId)
    {
        $similarProducts = Product::similar($productId)
            ->with('category')
            ->take(4)
            ->get();

        return response()->json([
            'products' => $similarProducts
        ]);
    }

    /**
     * Get featured products
     */
    public function featured()
    {
        $featuredProducts = Product::with('category')
            ->where('is_active', true)
            ->where('is_featured', true)
            ->take(8)
            ->get();

        return response()->json([
            'products' => $featuredProducts
        ]);
    }
}

## 6. Frontend Authentication

### Accessing Auth Data in React/TypeScript

In any Inertia page component, you can access the authenticated user data through shared props:

```tsx
import { usePage } from '@inertiajs/react';

interface User {
  id: number;
  name: string;
  email: string;
  role: 'ADMIN' | 'USER' | 'VENDOR';
}

interface PageProps {
  auth: {
    user: User | null;
  };
}

export default function Dashboard() {
  const { auth } = usePage().props as PageProps;

  const isAuthenticated = !!auth.user;
  const isAdmin = auth.user?.role === 'ADMIN';
  const isVendor = auth.user?.role === 'VENDOR';

  return (
    <div>
      {isAuthenticated ? (
        <p>Welcome, {auth.user.name}!</p>
      ) : (
        <p>Please log in</p>
      )}

      {isAdmin && <AdminPanel />}
      {isVendor && <VendorDashboard />}
    </div>
  );
}

Conditional UI Elements

Restricting content based on authentication:

{
  isAuthenticated ? (
    <button onClick={handleLogout}>Logout</button>
  ) : (
    <div>
      <Link href="/login">Login</Link>
      <Link href="/register">Register</Link>
    </div>
  );
}

Restricting content based on role:

{
  isAdmin && (
    <div className="admin-panel">
      <h2>Admin Controls</h2>
      <Link href="/admin/categories">Manage Categories</Link>
      <Link href="/admin/products">Manage Products</Link>
    </div>
  );
}

Dynamic Sidebar Navigation

Here's a complete implementation of a sidebar that shows different links based on user role:

import { usePage } from "@inertiajs/react";
import { Link } from "@inertiajs/react";

interface User {
  id: number;
  name: string;
  email: string;
  role: "ADMIN" | "USER" | "VENDOR";
}

interface PageProps {
  auth: {
    user: User | null;
  };
}

export default function Sidebar() {
  const { auth } = usePage().props as PageProps;
  const isAuthenticated = !!auth.user;
  const isAdmin = auth.user?.role === "ADMIN";
  const isVendor = auth.user?.role === "VENDOR";

  return (
    <nav className="sidebar bg-gray-800 text-white w-64 min-h-screen p-4">
      <div className="mb-8">
        <h2 className="text-xl font-bold">Your E-commerce</h2>
      </div>

      {/* Public links */}
      <div className="mb-6">
        <h3 className="uppercase text-xs tracking-wider text-gray-400 mb-2">
          Shop
        </h3>
        <ul className="space-y-2">
          <li>
            <Link
              href="/shop"
              className="block px-2 py-1 rounded hover:bg-gray-700"
            >
              Home
            </Link>
          </li>
          <li>
            <Link
              href="/shop/categories"
              className="block px-2 py-1 rounded hover:bg-gray-700"
            >
              Categories
            </Link>
          </li>
          <li>
            <Link
              href="/shop/products"
              className="block px-2 py-1 rounded hover:bg-gray-700"
            >
              All Products
            </Link>
          </li>
        </ul>
      </div>

      {/* Common links for all authenticated users */}
      {isAuthenticated && (
        <div className="mb-6">
          <h3 className="uppercase text-xs tracking-wider text-gray-400 mb-2">
            Account
          </h3>
          <ul className="space-y-2">
            <li>
              <Link
                href="/dashboard"
                className="block px-2 py-1 rounded hover:bg-gray-700"
              >
                Dashboard
              </Link>
            </li>
            <li>
              <Link
                href="/profile"
                className="block px-2 py-1 rounded hover:bg-gray-700"
              >
                Profile
              </Link>
            </li>
            <li>
              <Link
                href="/orders"
                className="block px-2 py-1 rounded hover:bg-gray-700"
              >
                My Orders
              </Link>
            </li>
          </ul>
        </div>
      )}

      {/* Admin-only links */}
      {isAdmin && (
        <div className="mb-6">
          <h3 className="uppercase text-xs tracking-wider text-gray-400 mb-2">
            Admin
          </h3>
          <ul className="space-y-2">
            <li>
              <Link
                href="/admin/dashboard"
                className="block px-2 py-1 rounded hover:bg-gray-700"
              >
                Admin Dashboard
              </Link>
            </li>
            <li>
              <Link
                href="/admin/categories"
                className="block px-2 py-1 rounded hover:bg-gray-700"
              >
                Manage Categories
              </Link>
            </li>
            <li>
              <Link
                href="/admin/products"
                className="block px-2 py-1 rounded hover:bg-gray-700"
              >
                Manage Products
              </Link>
            </li>
            <li>
              <Link
                href="/admin/users"
                className="block px-2 py-1 rounded hover:bg-gray-700"
              >
                Manage Users
              </Link>
            </li>
          </ul>
        </div>
      )}

      {/* Vendor-only links */}
      {isVendor && (
        <div className="mb-6">
          <h3 className="uppercase text-xs tracking-wider text-gray-400 mb-2">
            Vendor
          </h3>
          <ul className="space-y-2">
            <li>
              <Link
                href="/vendor/dashboard"
                className="block px-2 py-1 rounded hover:bg-gray-700"
              >
                Vendor Dashboard
              </Link>
            </li>
            <li>
              <Link
                href="/vendor/products"
                className="block px-2 py-1 rounded hover:bg-gray-700"
              >
                My Products
              </Link>
            </li>
            <li>
              <Link
                href="/vendor/orders"
                className="block px-2 py-1 rounded hover:bg-gray-700"
              >
                Orders
              </Link>
            </li>
          </ul>
        </div>
      )}

      {/* Authentication links */}
      <div className="mt-auto pt-6">
        {isAuthenticated ? (
          <Link
            href="/logout"
            method="post"
            as="button"
            className="w-full text-left px-2 py-1 rounded hover:bg-gray-700"
          >
            Logout
          </Link>
        ) : (
          <div className="space-y-2">
            <Link
              href="/login"
              className="block px-2 py-1 rounded hover:bg-gray-700"
            >
              Login
            </Link>
            <Link
              href="/register"
              className="block px-2 py-1 rounded hover:bg-gray-700"
            >
              Register
            </Link>
          </div>
        )}
      </div>
    </nav>
  );
}

7. Form Handling

Form Submission with Inertia

Inertia.js provides a useForm hook for managing form state, validation, and submissions. Here's a complete example of a category creation form:

import { useForm } from "@inertiajs/react";

interface CategoryForm {
  name: string;
  description: string;
  image: File | null;
  is_active: boolean;
}

export default function CreateCategory() {
  const form = useForm<CategoryForm>({
    name: "",
    description: "",
    image: null,
    is_active: true,
  });

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    form.post(route("admin.categories.store"));
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      <div>
        <label
          htmlFor="name"
          className="block text-sm font-medium text-gray-700"
        >
          Category Name
        </label>
        <input
          id="name"
          type="text"
          value={form.data.name}
          onChange={(e) => form.setData("name", e.target.value)}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
        />
        {form.errors.name && (
          <div className="text-red-500 text-sm mt-1">{form.errors.name}</div>
        )}
      </div>

      <div>
        <label
          htmlFor="description"
          className="block text-sm font-medium text-gray-700"
        >
          Description
        </label>
        <textarea
          id="description"
          value={form.data.description}
          onChange={(e) => form.setData("description", e.target.value)}
          rows={4}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
        />
        {form.errors.description && (
          <div className="text-red-500 text-sm mt-1">
            {form.errors.description}
          </div>
        )}
      </div>

      <div>
        <div className="flex items-center">
          <input
            id="is_active"
            type="checkbox"
            checked={form.data.is_active}
            onChange={(e) => form.setData("is_active", e.target.checked)}
            className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
          />
          <label
            htmlFor="is_active"
            className="ml-2 block text-sm text-gray-700"
          >
            Active
          </label>
        </div>
        {form.errors.is_active && (
          <div className="text-red-500 text-sm mt-1">
            {form.errors.is_active}
          </div>
        )}
      </div>

      <div>
        <label className="block text-sm font-medium text-gray-700">
          Category Image
        </label>
        <div className="mt-1 flex items-center">
          <input
            type="file"
            onChange={(e) => form.setData("image", e.target.files?.[0] || null)}
            className="block w-full text-sm text-gray-500
              file:mr-4 file:py-2 file:px-4
              file:rounded-md file:border-0
              file:text-sm file:font-semibold
              file:bg-indigo-50 file:text-indigo-700
              hover:file:bg-indigo-100"
          />
        </div>
        {form.errors.image && (
          <div className="text-red-500 text-sm mt-1">{form.errors.image}</div>
        )}
      </div>

      <div className="flex justify-end">
        <button
          type="submit"
          disabled={form.processing}
          className="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
        >
          {form.processing ? "Saving..." : "Create Category"}
        </button>
      </div>
    </form>
  );
}

Product Form with Multiple Images

Here's an example of a product form that includes multiple image uploads:

import { useForm } from '@inertiajs/react';

interface Category {
  id: number;
  name: string;
}

interface ProductForm {
  category_id: string;
  name: string;
  description: string;
  price: string;
  old_price: string;
  stock: string;
  image: File | null;
  gallery: File[];
  is_featured: boolean;
  is_active: boolean;
}

interface ProductFormProps {
  categories: Category[];
}

export default function CreateProduct({ categories }: ProductFormProps) {
  const form = useForm<ProductForm>({
    category_id: '',
    name: '',
    description: '',
    price: '',
    old_price: '',
    stock: '0',
    image: null,
    gallery: [],
    is_featured: false,
    is_active: true
  });

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    form.post(route('admin.products.store'));
  }

  function handleImageChange(e: React.ChangeEvent<HTMLInputElement>) {
    if (e.target.files?.[0]) {
      form.setData('image', e.target.files[0]);
    }
  }

  function handleGalleryChange(e: React.ChangeEvent<HTMLInputElement>) {
    const files = e.target.files;
    if (files) {
      // Convert FileList to Array for Inertia
      const fileArray = Array.from(files);
      form.setData('gallery', fileArray);
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      <div>
        <label htmlFor="category_id" className="block text-sm font-medium text-gray-700">
          Category
        </label>
        <select
          id="category_id"
          value={form.data.category_id}
          onChange={e => form.setData('category_id', e.target.value)}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
        >
          <option value="">Select a category</option>
          {categories.map(category => (
            <option key={category.id} value={category.id}>
              {category.name}
            </option>
          ))}
        </select>
        {form.errors.category_id && (
          <div className="text-red-500 text-sm mt-1">{form.errors.category_id}</div>
        )}
      </div>

      <div>
        <label htmlFor="name" className="block text-sm font-medium text-gray-700">
          Product Name
        </label>
        <input
          id="name"
          type="text"
          value={form.data.name}
          onChange={e => form.setData('name', e.target.value)}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
        />
        {form.errors.name && (
          <div className="text-red-500 text-sm mt-1">{form.errors.name}</div>
        )}
      </div>

      <div>
        <label htmlFor="description" className="block text-sm font-medium text-gray-700">
          Description
        </label>
        <textarea
          id="description"
          value={form.data.description}
          onChange={e => form.setData('description', e.target.value)}
          rows={4}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
        />
        {form.errors.description && (
          <div className="text-red-500 text-sm mt-1">{form.errors.description}</div>
        )}
      </div>

      <div className="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
        <div className="sm:col-span-2">
          <label htmlFor="price" className="block text-sm font-medium text-gray-700">
            Price
          </label>
          <div className="mt-1 relative rounded-md shadow-sm">
            <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
              <span className="text-gray-500 sm:text-sm">$</span>
            </div>
            <input
              type="text"
              id="price"
              value={form.data.price}
              onChange={e => form.setData('price', e.target.value)}
              className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-7 pr-12 sm:text-sm border-gray-300 rounded-md"
              placeholder="0.00"
            />
          </div>
          {form.errors.price && (
            <div className="text-red-500 text-sm mt-1">{form.errors.price}</div>
          )}
        </div>

        <div className="sm:col-span-2">
          <label htmlFor="old_price" className="block text-sm font-medium text-gray-700">
            Old Price (Optional)
          </label>
          <div className="mt-1 relative rounded-md shadow-sm">
            <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
              <span className="text-gray-500 sm:text-sm">$</span>
            </div>
            <input
              type="text"
              id="old_price"
              value={form.data.old_price}
              onChange={e => form.setData('old_price', e.target.value)}
              className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-7 pr-12 sm:text-sm border-gray-300 rounded-md"
              placeholder="0.00"
            />
          </div>
          {form.errors.old_price && (
            <div className="text-red-500 text-sm mt-1">{form.errors.old_price}</div>
          )}
        </div>

        <div className="sm:col-span-2">
          <label htmlFor="stock" className="block text-sm font-medium text-gray-700">
            Stock
          </label>
          <input
            type="number"
            id="stock"
            value={form.data.stock}
            onChange={e => form.setData('stock', e.target.value)}
            className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
          />
          {form.errors.stock && (
            <div className="text-red-500 text-sm mt-1">{form.errors.stock}</div>
          )}
        </div>
      </div>

      <div>
        <label className="block text-sm font-medium text-gray-700">
          Main Product Image
        </label>


use App\Http\Controllers\Controller;
use App\Models\Category;
use Illuminate\Http\Request;

class CategoryController extends Controller
{
    /**
     * Get all active categories
     */
    public function index()
    {
        $categories = Category::where('is_active', true)
            ->withCount('products')
            ->get();

        return response()->json([
            'categories' => $categories
        ]);
    }

    /**
     * Get a specific category by slug
     */
    public function show($slug)
    {
        $category = Category::where('slug', $slug)
            ->where('is_active', true)
            ->firstOrFail();

        return response()->json([
            'category' => $category
        ]);
    }

    /**
     * Get products for a specific category
     */
    public function products($slug, Request $request)
    {
        $category = Category::where('slug', $slug)
            ->where('is_active', true)
            ->firstOrFail();

        $query = $category->products()
            ->where('is_active', true);

        // Handle sorting
        $sort = $request->input('sort', 'latest');
        switch ($sort) {
            case 'price_low':
                $query->orderBy('price', 'asc');
                break;
            case 'price_high':
                $query->orderBy('price', 'desc');
                break;
            case 'name':
                $query->orderBy('name', 'asc');
                break;
            default:
                $query->latest();
                break;
        }

        $perPage = $request->input('per_page', 12);
        $products = $query->paginate($perPage);

        return response()->json([
            'category' => $category,
            'products' => $products
        ]);
    }
}

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Category;
use Illuminate\Http\Request;

class CategoryController extends Controller
{
    /**
     * Display a listing of the categories
     */
    public function index()
    {
        $categories = Category::where('is_active', true)
            ->withCount('products')
            ->get();

        return inertia('Shop/Categories/Index', [
            'categories' => $categories
        ]);
    }

    /**
     * Display products for a specific category
     */
    public function show($slug)
    {
        $category = Category::where('slug', $slug)
            ->where('is_active', true)
            ->firstOrFail();

        $products = $category->products()
            ->where('is_active', true)
            ->latest()
            ->paginate(12);

        return inertia('Shop/Categories/Show', [
            'category' => $category,
            'products' => $products
        ]);
    }
}