Setelah sebelumnya kita buat design schema database, kali jni kita akan implementasi CRUD produk dan kategori. Pembaca harus mengikuti tutorial ini sesuai part agar tidak terjadi error.

A. Menambahkan Model dan Relasi

Model Product dan Category

Buat model Product dan Category dengan perintah berikut:

php artisan make:model Product
php artisan make:model Category

Perintah ini akan menghasilkan dua file model (Product.php dan Category.php) dan file migrasi untuk masing-masing.

Relasi Many-to-Many di Model

Update model Product dan Category untuk mendefinisikan relasi.

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', 'slug','description', 'price', 'sku', 'image', 'brand_id','stock'];

    // Relasi Many-to-Many ke Category
    public function categories()
    {
        return $this->belongsToMany(Category::class, 'product_category');
    }
    public function brand()
    {
        return $this->belongsTo(Brand::class);
    }
}

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

    // Relasi Many-to-Many ke Product
    public function products()
    {
        return $this->belongsToMany(Product::class, 'product_category');
    }
}

B. Membuat Controller dan Views untuk CRUD

Membuat Controller

Gunakan perintah untuk membuat controller ProductController dan CategoryController:

php artisan make:controller ProductController --resource
php artisan make:controller CategoryController --resource

Controller-resource ini otomatis menyediakan method dasar untuk CRUD (index, create, store, edit, update, dan destroy).

Ubah ProductController menjadi sebagai berikut:

<?php

namespace App\Http\Controllers;

use App\Models\Product;
use App\Models\Category;
use App\Models\Brand;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class ProductController extends Controller
{
    public function index()
    {
        $products = Product::with('categories')->get();
        return view('admin.products.index', compact('products'));
    }

    public function create()
    {
        $categories = Category::all();
        $brands = Brand::all();
        return view('admin.products.create', compact('categories','brands'));
    }

    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'price' => 'required|numeric',
            'sku' => 'required|string|unique:products',
            'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
            'brand_id' => 'nullable|exists:brands,id',
        ]);

        $product = new Product($request->all());
        $product->slug = Str::slug($request->name);

        if ($request->hasFile('image')) {
            $imagePath = $request->file('image')->store('images', 'public');
            $product->image = $imagePath;
        }

        $product->save();
        $product->categories()->sync($request->categories);

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

    public function edit(Product $product)
    {
        $categories = Category::all();
        $brands = Brand::all();
        return view('admin.products.edit', compact('product', 'categories', 'brands'));
    }

    public function update(Request $request, Product $product)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'price' => 'required|numeric',
            'sku' => 'required|string|unique:products,sku,' . $product->id,
            'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
            'brand_id' => 'nullable|exists:brands,id',
        ]);

        $product->fill($request->all());
        $product->slug = Str::slug($request->name);

        if ($request->hasFile('image')) {
            $imagePath = $request->file('image')->store('images', 'public');
            $product->image = $imagePath;
        }

        $product->save();
        $product->categories()->sync($request->categories);

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

    public function destroy(Product $product)
    {
        $product->categories()->detach();
        $product->delete();
        return redirect()->route('products.index')->with('success', 'Product deleted successfully');
    }
}

Dan ubah CategoryController menjadi sebagai berikut:

<?php

namespace App\Http\Controllers;

use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class CategoryController extends Controller
{
    public function index()
    {
        $categories = Category::all();
        return view('admin.categories.index', compact('categories'));
    }

    public function create()
    {
        return view('admin.categories.create');
    }

    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|max:255|unique:categories',
        ]);

        $category = new Category();
        $category->name = $request->name;
        $category->slug = Str::slug($request->name);
        $category->save();

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

    public function edit(Category $category)
    {
        return view('admin.categories.edit', compact('category'));
    }

    public function update(Request $request, Category $category)
    {
        $request->validate([
            'name' => 'required|string|max:255|unique:categories,name,' . $category->id,
        ]);

        $category->name = $request->name;
        $category->slug = Str::slug($request->name);
        $category->save();

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

    public function destroy(Category $category)
    {
        $category->products()->detach();
        $category->delete();
        return redirect()->route('categories.index')->with('success', 'Category deleted successfully');
    }
}

Rute untuk Produk dan Kategori di Admin Panel

Buka file routes/web.php dan tambahkan rute untuk admin CRUD:

use App\Http\Controllers\CategoryController;
use App\Http\Controllers\ProductController;

Route::prefix('admin')->middleware('auth')->group(function () {
    Route::resource('products', ProductController::class);
    Route::resource('categories', CategoryController::class);
});

View untuk CRUD Produk dan Kategori

Buat struktur folder untuk view di resources/views/admin dengan subfolder products dan categories.


C. Membuat views CRUD di Admin Panel untuk Produk dan Kategori

1. Category CRUD

Buat empat file view untuk CRUD di resources/views/admin/categories:

  1. index.blade.php (Daftar Kategori)
  2. create.blade.php (Tambah Kategori)
  3. edit.blade.php (Edit Kategori)

Edit File: resources/views/admin/categories/index.blade.php

@extends('layouts.admin')

@section('content')
<h1>Category List</h1>
<a href="{{ route('categories.create') }}" class="btn btn-primary mb-3">Add New Category</a>
<table class="table table-bordered">
    <thead>
        <tr>
            <th>#</th>
            <th>Name</th>
            <th>Slug</th>
            <th>Actions</th>
        </tr>
    </thead>
    <tbody>
        @foreach ($categories as $category)
            <tr>
                <td>{{ $loop->iteration }}</td>
                <td>{{ $category->name }}</td>
                <td>{{ $category->slug }}</td>
                <td>
                    <a href="{{ route('categories.edit', $category->id) }}" class="btn btn-secondary">Edit</a>
                    <form action="{{ route('categories.destroy', $category->id) }}" method="POST" style="display:inline;">
                        @csrf
                        @method('DELETE')
                        <button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure?')">Delete</button>
                    </form>
                </td>
            </tr>
        @endforeach
    </tbody>
</table>
@endsection

Edit File: resources/views/admin/categories/create.blade.php

@extends('layouts.admin')

@section('content')
<h1>Add New Category</h1>
<form action="{{ route('categories.store') }}" method="POST">
    @csrf
    <div class="form-group mb-3">
        <label for="name">Category Name:</label>
        <input type="text" name="name" class="form-control" required>
    </div>
    <button type="submit" class="btn btn-primary">Save Category</button>
    <a href="{{ route('categories.index') }}" class="btn btn-secondary">Back to Categories</a>
</form>
@endsection

File: resources/views/admin/categories/edit.blade.php

@extends('layouts.admin')

@section('content')
<h1>Edit Category</h1>
<form action="{{ route('categories.update', $category->id) }}" method="POST">
    @csrf
    @method('PUT')
    <div class="form-group mb-3">
        <label for="name">Category Name:</label>
        <input type="text" name="name" class="form-control" value="{{ $category->name }}" required>
    </div>
    <button type="submit" class="btn btn-primary">Update Category</button>
    <a href="{{ route('categories.index') }}" class="btn btn-secondary">Back to Categories</a>
</form>
@endsection

2. Products views  CRUD

Buat empat file view untuk CRUD di resources/views/admin/products:

  1. index.blade.php (Daftar Produk)
  2. create.blade.php (Tambah Produk)
  3. edit.blade.php (Edit Produk)

Edit File: resources/views/admin/products/index.blade.php

@extends('layouts.admin')

@section('content')
<h1>Product List</h1>
<a href="{{ route('products.create') }}" class="btn btn-primary">Add New Product</a>
@if (session('success'))
    <div class="alert alert-success">{{ session('success') }}</div>
@endif
<table class="table">
    <thead>
        <tr>
            <th>Name</th>
            <th>Price</th>
            <th>Categories</th>
            <th>Image</th>
            <th>Actions</th>
        </tr>
    </thead>
    <tbody>
        @foreach ($products as $product)
            <tr>
                <td>{{ $product->name }}</td>
                <td>${{ $product->price }}</td>
                <td>{{ $product->categories->pluck('name')->join(', ') }}</td>
                <td>
                    @if($product->image)
                        <img src="{{ asset('storage/' . $product->image) }}" width="50" alt="{{ $product->name }}">
                    @endif
                </td>
                <td>
                    <a href="{{ route('products.edit', $product->id) }}" class="btn btn-secondary">Edit</a>
                    <form action="{{ route('products.destroy', $product->id) }}" method="POST" style="display:inline;">
                        @csrf
                        @method('DELETE')
                        <button type="submit" class="btn btn-danger">Delete</button>
                    </form>
                </td>
            </tr>
        @endforeach
    </tbody>
</table>
@endsection

Edit File: resources/views/admin/products/create.blade.php

@extends('layouts.admin')

@section('content')
<h1>Add New Product</h1>
<form action="{{ route('products.store') }}" method="POST" enctype="multipart/form-data">
    @csrf
    <div class="form-group">
        <label for="name">Product Name:</label>
        <input type="text" name="name" class="form-control" required>
    </div>
    <div class="form-group">
        <label for="name">Product SKU:</label>
        <input type="text" name="sku" class="form-control" required>
    </div>
    <div class="form-group">
        <label for="price">Price:</label>
        <input type="number" name="price" class="form-control" required>
    </div>
    <div class="form-group">
        <label for="categories">Brands:</label>
        <select name="brand_id" id="brand_id" class="form-control">
            <option value="">Select Brand</option>
            @foreach($brands as $brand)
                <option value="{{ $brand->id }}" {{ isset($product) && $product->brand_id == $brand->id ? 'selected' : '' }}>
                    {{ $brand->name }}
                </option>
            @endforeach
        </select>
    </div>
    <div class="form-group">
        <label for="categories">Categories:</label>
        <select name="categories[]" class="form-control" multiple>
            @foreach ($categories as $category)
                <option value="{{ $category->id }}">{{ $category->name }}</option>
            @endforeach
        </select>
    </div>
    <div class="form-group">
        <label for="description">Description:</label>
        <textarea name="description" class="form-control"></textarea>
    </div>
    <div class="form-group">
        <label for="image">Image:</label>
        <input type="file" name="image" class="form-control">
    </div>
    <button type="submit" class="btn btn-primary">Save Product</button>
</form>
@endsection

Edit File: resources/views/admin/products/edit.blade.php

@extends('layouts.admin')

@section('content')
<h1>Edit Product</h1>
<form action="{{ route('products.update', $product->id) }}" method="POST" enctype="multipart/form-data">
    @csrf
    @method('PUT')
    <div class="form-group">
        <label for="name">Product Name:</label>
        <input type="text" name="name" class="form-control" value="{{ $product->name }}" required>
    </div>
    <div class="form-group">
        <label for="name">Product Name:</label>
        <input type="text" name="sku" class="form-control" value="{{ $product->sku }}" required>
    </div>
    <div class="form-group">
        <label for="price">Price:</label>
        <input type="number" name="price" class="form-control" value="{{ $product->price }}" required>
    </div>
    <div class="form-group">
        <label for="categories">Brands:</label>
        <select name="brand_id" id="brand_id" class="form-control">
            <option value="">Select Brand</option>
            @foreach($brands as $brand)
                <option value="{{ $brand->id }}" {{ isset($product) && $product->brand_id == $brand->id ? 'selected' : '' }}>
                    {{ $brand->name }}
                </option>
            @endforeach
        </select>
    </div>
    <div class="form-group">
        <label for="categories">Categories:</label>
        <select name="categories[]" class="form-control" multiple>
            @foreach ($categories as $category)
                <option value="{{ $category->id }}" {{ in_array($category->id, $product->categories->pluck('id')->toArray()) ? 'selected' : '' }}>
                    {{ $category->name }}
                </option>
            @endforeach
        </select>
    </div>
    <div class="form-group">
        <label for="description">Description:</label>
        <textarea name="description" class="form-control">{{ $product->description }}</textarea>
    </div>
    <div class="form-group">
        <label for="image">Image:</label>
        <input type="file" name="image" class="form-control">
    </div>
    <button type="submit" class="btn btn-primary">Update Product</button>
</form>
@endsection

D. Admin Panel Design

Menambahkan Sidebar dan Layout

Buat file layouts/admin.blade.php untuk layout utama admin dengan sidebar dan navbar.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Admin Dashboard</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
    <link href="{{ asset('css/admin.css') }}" rel="stylesheet" type="text/css" >
    <!-- Tambahkan ini di head atau di bagian bawah file layout Anda -->
</head>
<body>
    <!-- Sidebar Toggle Button (visible on mobile) -->
    <button id="sidebarToggle" class="btn">
        <i class="fa fa-bars"></i>
    </button>

    <!-- Sidebar -->
    <div id="sidebar">
        <h4 class="text-center mt-3">Admin Panel</h4>
        <nav class="nav flex-column mt-4">
            <a class="nav-link {{ request()->is('dashboard') ? 'active' : '' }}" href="{{ route('admin.dashboard') }}">
                <i class="fa fa-tachometer-alt"></i> Dashboard
            </a>
            <a class="nav-link {{ request()->is('brands*') ? 'active' : '' }}" href="{{ route('brands.index') }}">
                <i class="fa fa-tags"></i> Brands
            </a>
            <a class="nav-link {{ request()->is('categories*') ? 'active' : '' }}" href="{{ route('categories.index') }}">
                <i class="fa fa-th-list"></i> Categories
            </a>
            <a class="nav-link {{ request()->is('products*') ? 'active' : '' }}" href="{{ route('products.index') }}">
                <i class="fa fa-box-open"></i> Products
            </a>
            <a class="nav-link" href="{{ route('logout') }}">
                <i class="fa fa-sign-out-alt"></i> Logout
            </a>
        </nav>
    </div>

    <!-- Main Content -->
    <div id="contents">
        <div class="container-fluid">
            @yield('content')
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    <script>
        // Toggle sidebar visibility on mobile
        document.getElementById('sidebarToggle').addEventListener('click', function() {
            document.getElementById('sidebar').classList.toggle('active');
            document.getElementById('contents').classList.toggle('active');
        });
    </script>
     @yield('script')
</body>
</html>

Sidebar dan Navbar Styling (CSS)

Buat file CSS di public/css/admin.css untuk styling sederhana admin panel:

#sidebar {
    width: 250px;
    height: 100vh;
    position: fixed;
    top: 0;
    left: 0;
    background-color: #1e3d59;
    color: #fff;
    padding-top: 20px;
    z-index: 1000;
    transition: all 0.3s;
}
#sidebar .nav-link {
    color: #cdd3d8;
    font-size: 16px;
    padding: 15px 20px;
    display: flex;
    align-items: center;
}
#sidebar .nav-link .fa {
    margin-right: 10px;
    font-size: 18px;
}
#sidebar .nav-link.active, #sidebar .nav-link:hover {
    background-color: #0b2638;
    color: #fff;
}
/* Content area */
#contents {
    margin-left: 250px;
    padding: 20px;
    background-color: #f8f9fa;
    min-height: 100vh;
    transition: margin-left 0.3s;
}
/* Sidebar toggle button for mobile */
#sidebarToggle {
    display: none;
}

@media (max-width: 768px) {
    #sidebar {
        left: -250px;
    }
    #sidebar.active {
        left: 0;
    }
    #contents {
        margin-left: 0;
    }
    #contents.active {
        margin-left: 250px;
    }
    #sidebarToggle {
        display: block;
        position: fixed;
        top: 10px;
        left: 10px;
        z-index: 1100;
        font-size: 24px;
        color: #1e3d59;
        background-color: #fff;
        border: none;
    }
}

Hasil akhir akan seperti berikut:

http://localhost:8000/categories

http://localhost:8000/products/create

http://localhost:8000/products

Selamat Mencoba!! jangan lupa tinggalkan komentar jika ada yang mau ditanyakan :)