Buat Latihan project Laravel marketplace minimal, siap dijalankan di XAMPP, dan saya sertakan semua file penting yang kamu butuh untuk menjalankan MVP (auth, produk, kategori, cart session, checkout sederhana, admin CRUD, seeder, storage link, dan instruksi).
Di bawah ini saya paste file-by-file yang perlu kamu buat/masukkan ke project (salin-tempel). Ikuti urutan supaya lebih rapi.
Asumsi: kamu pakai Laravel 10+, PHP ≥ 8.1, MySQL via XAMPP, project diletakkan di
C:\xampp\htdocs\marketplace(Windows) atau/opt/lampp/htdocs/marketplace(Linux).
Jika mau, setelah ini saya juga bisa rapikan jadi checklist file yang bisa kamu copy-paste satu per satu.
1) FILE: .env.example
Salin ke .env lalu sesuaikan.
APP_NAME=Marketplace
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8000
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=marketplace_db
DB_USERNAME=root
DB_PASSWORD=
FILESYSTEM_DRIVER=public
2) README singkat (README.md)
# Marketplace (Laravel) - Minimal MVP
Requirements:
- PHP 8.1+, Composer, Node.js (optional for assets)
- XAMPP (Apache + MySQL)
Setup:
1. letakkan folder project di htdocs
2. copy .env.example -> .env, set DB
3. run: composer install
4. run: php artisan key:generate
5. run: php artisan migrate --seed
6. run: php artisan storage:link
7. run: php artisan serve --port=8000 (atau akses via virtualhost XAMPP)
3) MIGRATIONS (database/migrations)
a) 2025_01_01_000001_add_is_admin_to_users_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(){
Schema::table('users', function(Blueprint $table){
$table->boolean('is_admin')->default(false)->after('password');
});
}
public function down(){
Schema::table('users', function(Blueprint $table){
$table->dropColumn('is_admin');
});
}
};
b) 2025_01_01_000002_create_categories_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(){
Schema::create('categories', function(Blueprint $table){
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
}
public function down(){ Schema::dropIfExists('categories'); }
};
c) 2025_01_01_000003_create_products_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(){
Schema::create('products', function(Blueprint $table){
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->decimal('price',12,2);
$table->integer('stock')->default(0);
$table->string('image')->nullable();
$table->foreignId('category_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
}
public function down(){ Schema::dropIfExists('products'); }
};
d) 2025_01_01_000004_create_orders_and_order_items.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(){
Schema::create('orders', function(Blueprint $table){
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->decimal('total',12,2);
$table->string('status')->default('pending');
$table->string('payment_method')->nullable();
$table->timestamps();
});
Schema::create('order_items', function(Blueprint $table){
$table->id();
$table->foreignId('order_id')->constrained()->onDelete('cascade');
$table->foreignId('product_id')->constrained()->onDelete('cascade');
$table->integer('qty');
$table->decimal('price',12,2);
$table->timestamps();
});
}
public function down(){
Schema::dropIfExists('order_items');
Schema::dropIfExists('orders');
}
};
4) MODELS (app/Models)
a) 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); }
}
b) 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','stock','image','category_id'];
public function category(){ return $this->belongsTo(Category::class); }
}
c) 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','payment_method'];
public function items(){ return $this->hasMany(OrderItem::class); }
public function user(){ return $this->belongsTo(User::class); }
}
d) 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','qty','price'];
public function product(){ return $this->belongsTo(Product::class); }
}
5) CONTROLLERS (app/Http/Controllers)
a) HomeController.php
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\Category;
class HomeController extends Controller {
public function index(){
$categories = Category::withCount('products')->get();
$products = Product::with('category')->orderBy('created_at','desc')->paginate(12);
return view('home', compact('products','categories'));
}
}
b) ProductController.php
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller {
public function index(Request $r){
$q = $r->query('q');
$products = Product::with('category')
->when($q, fn($db)=>$db->where('name','like','%'.$q.'%'))
->paginate(12);
return view('products.index', compact('products'));
}
public function show($slug){
$product = Product::where('slug',$slug)->with('category')->firstOrFail();
return view('products.show', compact('product'));
}
}
c) CartController.php
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class CartController extends Controller {
public function index(){ $cart = session('cart', []); return view('cart.index', compact('cart')); }
public function add(Request $r, $id){
$product = Product::findOrFail($id);
$cart = session('cart', []);
if(isset($cart[$id])) $cart[$id]['qty']++;
else $cart[$id] = ['id'=>$id,'name'=>$product->name,'price'=>$product->price,'qty'=>1,'image'=>$product->image];
session(['cart'=>$cart]);
return redirect()->back()->with('success','Produk ditambahkan ke keranjang');
}
public function update(Request $r, $id){
$cart = session('cart', []);
if(isset($cart[$id])){ $cart[$id]['qty'] = max(1,(int)$r->qty); session(['cart'=>$cart]); }
return back();
}
public function remove($id){
$cart = session('cart', []);
if(isset($cart[$id])){ unset($cart[$id]); session(['cart'=>$cart]); }
return back();
}
}
d) CheckoutController.php
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use Illuminate\Http\Request;
class CheckoutController extends Controller {
public function index(){ $cart = session('cart', []); if(empty($cart)) return redirect()->route('cart.index')->with('error','Keranjang kosong'); return view('checkout.index', compact('cart')); }
public function process(Request $r){
$user = auth()->user();
if(!$user) return redirect()->route('login');
$cart = session('cart', []);
if(empty($cart)) return redirect()->route('cart.index')->with('error','Keranjang kosong');
$total = collect($cart)->sum(fn($i)=> $i['price'] * $i['qty']);
$order = Order::create(['user_id'=>$user->id,'total'=>$total,'status'=>'pending','payment_method'=>$r->payment_method ?? 'manual']);
foreach($cart as $item){
OrderItem::create(['order_id'=>$order->id,'product_id'=>$item['id'],'qty'=>$item['qty'],'price'=>$item['price']]);
$p = Product::find($item['id']);
if($p) $p->decrement('stock', $item['qty']);
}
session()->forget('cart');
return redirect()->route('home')->with('success','Order berhasil dibuat. ID: '.$order->id);
}
}
e) Admin/ProductAdminController.php
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Product;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class ProductAdminController extends Controller {
public function __construct(){ $this->middleware(['auth','is_admin']); }
public function index(){ $products = Product::with('category')->paginate(20); return view('admin.products.index', compact('products')); }
public function create(){ $categories = Category::all(); return view('admin.products.create', compact('categories')); }
public function store(Request $r){
$r->validate(['name'=>'required','slug'=>'required|unique:products','price'=>'required|numeric','stock'=>'required|integer','category_id'=>'required']);
$data = $r->only(['name','slug','description','price','stock','category_id']);
if($r->hasFile('image')) $data['image'] = $r->file('image')->store('products','public');
Product::create($data);
return redirect()->route('admin.products.index')->with('success','Produk ditambahkan');
}
public function edit($id){ $product = Product::findOrFail($id); $categories = Category::all(); return view('admin.products.edit', compact('product','categories')); }
public function update(Request $r,$id){
$product = Product::findOrFail($id);
$r->validate(['name'=>'required','slug'=>'required|unique:products,slug,'.$product->id,'price'=>'required|numeric','stock'=>'required|integer','category_id'=>'required']);
$data = $r->only(['name','slug','description','price','stock','category_id']);
if($r->hasFile('image')){
if($product->image) Storage::disk('public')->delete($product->image);
$data['image'] = $r->file('image')->store('products','public');
}
$product->update($data);
return redirect()->route('admin.products.index')->with('success','Produk diupdate');
}
public function destroy($id){
$product = Product::findOrFail($id);
if($product->image) Storage::disk('public')->delete($product->image);
$product->delete();
return back()->with('success','Produk dihapus');
}
}
6) MIDDLEWARE IsAdmin (app/Http/Middleware/IsAdmin.php)
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class IsAdmin {
public function handle(Request $request, Closure $next){
if(auth()->check() && auth()->user()->is_admin) return $next($request);
abort(403,'Unauthorized');
}
}
Daftarkan di app/Http/Kernel.php pada $routeMiddleware:
'is_admin' => \App\Http\Middleware\IsAdmin::class,
7) ROUTES (routes/web.php)
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\ProductController;
use App\Http\Controllers\CartController;
use App\Http\Controllers\CheckoutController;
use App\Http\Controllers\Admin\ProductAdminController;
Route::get('/', [HomeController::class,'index'])->name('home');
Route::get('/products', [ProductController::class,'index'])->name('products.index');
Route::get('/product/{slug}', [ProductController::class,'show'])->name('product.show');
Route::get('/cart', [CartController::class,'index'])->name('cart.index');
Route::post('/cart/add/{id}', [CartController::class,'add'])->name('cart.add');
Route::post('/cart/update/{id}', [CartController::class,'update'])->name('cart.update');
Route::post('/cart/remove/{id}', [CartController::class,'remove'])->name('cart.remove');
Route::middleware(['auth'])->group(function(){
Route::get('/checkout', [CheckoutController::class,'index'])->name('checkout.index');
Route::post('/checkout', [CheckoutController::class,'process'])->name('checkout.process');
});
Route::prefix('admin')->middleware(['auth','is_admin'])->group(function(){
Route::resource('products', ProductAdminController::class, ['as'=>'admin']);
});
require __DIR__.'/auth.php';
8) VIEWS — penting (simpan di resources/views)
Sebagian view utama; sesuaikan CSS/JS jika perlu.
a) layouts/app.blade.php
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>@yield('title','Marketplace')</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <div class="container"> <a class="navbar-brand" href="{{ route('home') }}">Marketplace</a> <div class="collapse navbar-collapse"> <ul class="navbar-nav ms-auto"> <li class="nav-item"><a class="nav-link" href="{{ route('products.index') }}">Produk</a></li> <li class="nav-item"><a class="nav-link" href="{{ route('cart.index') }}">Keranjang ({{ count(session('cart',[])) }})</a></li> @auth @if(auth()->user()->is_admin) <li class="nav-item"><a class="nav-link" href="{{ url('admin/products') }}">Admin</a></li> @endif <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">{{ auth()->user()->name }}</a> <ul class="dropdown-menu"> <li><a class="dropdown-item" href="#" onclick="event.preventDefault();document.getElementById('logout-form').submit();">Logout</a></li> </ul> </li> <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display:none">@csrf</form> @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 mt-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> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> </html>
b) home.blade.php
@extends('layouts.app') @section('title','Home') @section('content') <div class="row"> <div class="col-md-3"> <h5>Kategori</h5> <ul class="list-group"> @foreach($categories as $c) <li class="list-group-item d-flex justify-content-between align-items-center">{{ $c->name }} <span class="badge bg-primary rounded-pill">{{ $c->products_count }}</span></li> @endforeach </ul> </div> <div class="col-md-9"> <div class="row"> @foreach($products as $p) <div class="col-md-4 mb-3"> <div class="card"> <img src="{{ $p->image ? asset('storage/'.$p->image) : 'https://via.placeholder.com/400x300' }}" class="card-img-top" style="height:200px;object-fit:cover"/> <div class="card-body"> <h5 class="card-title">{{ $p->name }}</h5> <p>Rp {{ number_format($p->price,0,',','.') }}</p> <a href="{{ route('product.show',$p->slug) }}" class="btn btn-sm btn-primary">Detail</a> <form action="{{ route('cart.add',$p->id) }}" method="POST" style="display:inline">@csrf<button class="btn btn-success btn-sm">Tambah</button></form> </div> </div> </div> @endforeach </div> {{ $products->links() }} </div> </div> @endsection
c) products/index.blade.php
(See previous home product listing — you can reuse.)
d) products/show.blade.php
@extends('layouts.app') @section('content') <div class="row"> <div class="col-md-6"><img src="{{ $product->image ? asset('storage/'.$product->image) : 'https://via.placeholder.com/600x400' }}" class="img-fluid"/></div> <div class="col-md-6"> <h3>{{ $product->name }}</h3> <p>Rp {{ number_format($product->price,0,',','.') }}</p> <p>Stok: {{ $product->stock }}</p> <p>{{ $product->description }}</p> <form action="{{ route('cart.add',$product->id) }}" method="POST">@csrf<button class="btn btn-success">Tambah ke Keranjang</button></form> </div> </div> @endsection
e) cart/index.blade.php
@extends('layouts.app') @section('content') <h4>Keranjang</h4> @if(empty($cart))<p>Keranjang kosong.</p> @else <table class="table"><thead><tr><th>Produk</th><th>Harga</th><th>Qty</th><th>Subtotal</th><th></th></tr></thead><tbody> @php $total=0; @endphp @foreach($cart as $c) @php $subtotal = $c['price'] * $c['qty']; $total += $subtotal; @endphp <tr> <td>{{ $c['name'] }}</td> <td>Rp {{ number_format($c['price'],0,',','.') }}</td> <td> <form action="{{ route('cart.update',$c['id']) }}" method="POST" class="d-flex">@csrf <input type="number" name="qty" value="{{ $c['qty'] }}" min="1" class="form-control form-control-sm me-2" style="width:80px"> <button class="btn btn-sm btn-primary">Update</button> </form> </td> <td>Rp {{ number_format($subtotal,0,',','.') }}</td> <td><form action="{{ route('cart.remove',$c['id']) }}" method="POST">@csrf<button class="btn btn-sm btn-danger">Hapus</button></form></td> </tr> @endforeach </tbody></table> <div class="d-flex justify-content-between"> <h5>Total: Rp {{ number_format($total,0,',','.') }}</h5> <a href="{{ route('checkout.index') }}" class="btn btn-success">Checkout</a> </div> @endif @endsection
f) checkout/index.blade.php
@extends('layouts.app') @section('content') <h4>Checkout</h4> @if(empty($cart)) <p>Keranjang kosong</p> @else <form action="{{ route('checkout.process') }}" method="POST">@csrf <div class="mb-3"> <label>Pilih Metode Pembayaran</label> <select name="payment_method" class="form-control"> <option value="bank_transfer">Transfer Bank</option> <option value="cod">Bayar di Tempat (COD)</option> </select> </div> <button class="btn btn-primary">Proses Pesanan</button> </form> @endif @endsection
g) Admin views: resources/views/admin/products/*
(Admin index/create/edit — saya sudah sediakan contoh sebelumnya in the earlier message; paste them into respective files.)
9) SEEDER (database/seeders/SampleSeeder.php)
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
use App\Models\Category;
use App\Models\Product;
use App\Models\User;
class SampleSeeder extends Seeder {
public function run(){
// Admin user (password: password123)
User::create([
'name'=>'Admin',
'email'=>'admin@example.com',
'password'=>bcrypt('password123'),
'is_admin'=>true
]);
User::create([
'name'=>'User',
'email'=>'user@example.com',
'password'=>bcrypt('password123'),
'is_admin'=>false
]);
$c1 = Category::create(['name'=>'Elektronik','slug'=>Str::slug('Elektronik')]);
$c2 = Category::create(['name'=>'Fashion','slug'=>Str::slug('Fashion')]);
Product::create(['name'=>'Headphone ABC','slug'=>'headphone-abc','description'=>'Headphone berkualitas','price'=>250000,'stock'=>10,'category_id'=>$c1->id]);
Product::create(['name'=>'Kaos Polos','slug'=>'kaos-polos','description'=>'Kaos cotton 100%','price'=>50000,'stock'=>50,'category_id'=>$c2->id]);
}
}
Update DatabaseSeeder.php agar memanggil SampleSeeder.
10) Perintah penting setelah menaruh file
Jalankan di terminal project:
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate --seed
php artisan storage:link
php artisan serve --port=8000
Atau jika di XAMPP, akses http://localhost/marketplace/public (jika meletakkan di htdocs tanpa virtualhost).
Akun admin: admin@example.com / password123
11) Opsi fitur tambahan (opsional, nanti bila ingin dikembangkan)
-
Integrasi payment gateway (Midtrans / Xendit / Stripe).
-
Upload product gallery (multiple images).
-
Seller accounts / multi-vendor (setiap UMKM punya dashboard sendiri).
-
Rating & review, pencarian lanjutan, filter berdasarkan kategori & harga.
-
Tema (themes folder + settings table untuk kustomisasi warna/logo via admin).
- Get link
- X
- Other Apps
Labels
UMKM- Get link
- X
- Other Apps

Comments
Post a Comment