Apa itu Laravel 13?
Laravel adalah framework PHP open-source yang dipakai untuk membangun aplikasi web modern dengan cepat dan rapi. Laravel mengikuti pola arsitektur MVC (Model-View-Controller), sehingga kode untuk data (Model), tampilan (View), dan logika (Controller) dipisah dengan jelas.
Laravel 13 melanjutkan arah yang sudah dibawa sejak Laravel 11: struktur folder yang jauh lebih ramping (tanpa banyak file konfigurasi yang jarang disentuh), bootstrap/app.php sebagai satu titik konfigurasi aplikasi, dan dukungan penuh untuk PHP versi terbaru.
Kenapa memilih Laravel?
Eloquent ORM
Berinteraksi dengan database pakai kode PHP biasa, tanpa nulis SQL manual.
Blade Templating
Sistem template yang ringan, mendukung layout, komponen, dan perulangan dengan sintaks bersih.
Artisan CLI
Perintah baris-perintah untuk generate file, migrasi database, dan tugas rutin lain.
Ekosistem luas
Breeze, Fortify, Sanctum, Livewire, Filament โ semua terintegrasi rapi dengan Laravel.
Instalasi & Konfigurasi Awal
Kebutuhan sistem
- PHP 8.3 atau lebih baru
- Composer (dependency manager PHP)
- Database โ MySQL, MariaDB, PostgreSQL, atau SQLite
- Node.js & npm (untuk kompilasi aset frontend lewat Vite)
Membuat project baru
Install lewat Composer
# cara 1: pakai Laravel installer
composer global require laravel/installer
laravel new nama-project
# cara 2: langsung via composer create-project
composer create-project laravel/laravel nama-projectMasuk ke folder project & jalankan server
cd nama-project
php artisan serveBuka http://127.0.0.1:8000 di browser โ kalau muncul halaman selamat datang Laravel, instalasi berhasil.
Atur koneksi database di file .env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=nama_database
DB_USERNAME=root
DB_PASSWORD=Setelah database dibuat (misalnya lewat phpMyAdmin), jalankan migrasi bawaan Laravel:
php artisan migrate.env, jalankan php artisan config:clear agar perubahan langsung terbaca.Struktur Folder Project
Laravel 13 memakai struktur ramping ala Laravel 11+. Berikut folder dan file yang paling sering kamu sentuh sehari-hari:
| Path | Fungsi |
|---|---|
app/Http/Controllers | Semua Controller diletakkan di sini |
app/Models | Semua Model Eloquent |
app/Livewire | Komponen Livewire (dibuat otomatis saat Livewire di-install) |
routes/web.php | Daftar route untuk halaman web |
routes/api.php | Daftar route untuk API (perlu diaktifkan dulu) |
resources/views | File Blade (.blade.php) โ tampilan HTML |
database/migrations | Struktur tabel database, versi-terkontrol |
database/seeders | Data awal / dummy untuk mengisi tabel |
public | Folder yang diakses browser (berisi index.php, aset publik) |
bootstrap/app.php | Titik konfigurasi utama aplikasi (routing, middleware, exception) |
.env | Variabel environment (koneksi database, mail, dsb) |
app/Http/Kernel.php untuk daftar middleware. Di Laravel 13, semua itu didaftarkan langsung di bootstrap/app.php.Routing
Route adalah pintu masuk: memetakan sebuah URL ke logika yang harus dijalankan. Semua route web didefinisikan di routes/web.php.
use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
// route sederhana ke closure
Route::get('/tentang', function () {
return 'Halaman Tentang Kami';
});
// route menuju method di Controller
Route::get('/produk', [ProductController::class, 'index']);
// route dengan parameter
Route::get('/produk/{id}', [ProductController::class, 'show']);
// resource route: otomatis bikin 7 route CRUD sekaligus
Route::resource('produk', ProductController::class);Method HTTP yang umum dipakai
| Method | Dipakai untuk |
|---|---|
Route::get() | Menampilkan halaman / mengambil data |
Route::post() | Mengirim / menyimpan data baru (dari form) |
Route::put() / patch() | Memperbarui data |
Route::delete() | Menghapus data |
Route resource โ daftar lengkap
Route::resource('produk', ProductController::class) otomatis membuat 7 route berikut:
| Verb | URI | Method Controller | Fungsi |
|---|---|---|---|
| GET | /produk | index | Daftar semua data |
| GET | /produk/create | create | Form tambah data |
| POST | /produk | store | Simpan data baru |
| GET | /produk/{id} | show | Detail satu data |
| GET | /produk/{id}/edit | edit | Form ubah data |
| PUT/PATCH | /produk/{id} | update | Simpan perubahan |
| DELETE | /produk/{id} | destroy | Hapus data |
Controller
Controller menampung logika aplikasi, supaya route tetap ringkas. Buat controller lewat Artisan:
php artisan make:controller ProductController --resourceOpsi --resource otomatis membuatkan 7 method kosong (index, create, store, show, edit, update, destroy) sesuai konvensi resource route.
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index()
{
$products = Product::latest()->paginate(10);
return view('products.index', compact('products'));
}
public function create()
{
return view('products.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
]);
Product::create($validated);
return redirect()->route('products.index')
->with('success', 'Produk berhasil ditambahkan.');
}
}Blade Templating
Blade adalah mesin template Laravel. File Blade berakhiran .blade.php dan disimpan di resources/views.
Sintaks dasar
{{-- menampilkan variabel (otomatis di-escape, aman dari XSS) --}}
{{ $product->name }}
{{-- kondisi --}}
@if($product->stock > 0)
Stok tersedia
@else
Stok habis
@endif
{{-- perulangan --}}
@foreach($products as $product)
{{ $product->name }}
@endforeachLayout & pewarisan template
Blade memakai konsep layout induk yang bisa "diwariskan" oleh halaman lain, supaya header/footer tidak ditulis berulang.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>@yield('title', 'Aplikasi Saya')</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container py-4">
@yield('content')
</div>
</body>
</html>@extends('layouts.app')
@section('title', 'Daftar Produk')
@section('content')
<h1>Daftar Produk</h1>
@endsectionKomponen Blade
Untuk elemen yang dipakai berulang (tombol, card, alert), gunakan komponen:
php artisan make:component Alert<x-alert type="success">Data berhasil disimpan</x-alert>Middleware
Middleware adalah lapisan penyaring yang dijalankan sebelum (atau sesudah) request sampai ke Controller โ misalnya untuk mengecek apakah user sudah login.
php artisan make:middleware CekUmurpublic function handle(Request $request, Closure $next)
{
if ($request->age < 18) {
return redirect('/');
}
return $next($request);
}Daftarkan alias middleware di bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'cek.umur' => \App\Http\Middleware\CekUmur::class,
]);
})Lalu pakai di route:
Route::get('/khusus-dewasa', [PageController::class, 'index'])
->middleware('cek.umur');auth (harus login), guest (harus belum login), throttle (batasi jumlah request).Request & Validasi
Semua data yang dikirim user (lewat form atau URL) bisa diakses lewat object Request.
public function store(Request $request)
{
$nama = $request->input('name'); // atau $request->name
$semua = $request->all(); // ambil semua input
}Validasi
Validasi memastikan data yang masuk sesuai aturan sebelum disimpan ke database.
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'price' => 'required|numeric|min:0',
'photo' => 'nullable|image|max:2048',
]);Kalau validasi gagal, Laravel otomatis mengalihkan kembali ke form sebelumnya beserta pesan error, yang bisa ditampilkan di Blade:
@error('name')
<div class="text-danger">{{ $message }}</div>
@enderrorForm Request (validasi terpisah, untuk form yang kompleks)
php artisan make:request StoreProductRequestLalu di Controller cukup ganti tipe parameter menjadi StoreProductRequest $request โ Laravel otomatis menjalankan validasinya sebelum masuk ke method.
Migration
Migration adalah "kontrol versi" untuk struktur database โ jadi struktur tabel ditulis dalam kode PHP, bukan diklik manual di phpMyAdmin, sehingga mudah dibagikan ke tim lain.
php artisan make:migration create_products_tablepublic function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->decimal('price', 10, 2);
$table->integer('stock')->default(0);
$table->string('photo')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}# jalankan semua migration yang belum dijalankan
php artisan migrate
# hapus semua tabel lalu jalankan ulang dari awal
php artisan migrate:fresh| Tipe kolom | Keterangan |
|---|---|
$table->string('nama') | VARCHAR, teks pendek |
$table->text('nama') | Teks panjang |
$table->integer() / decimal() | Angka bulat / desimal |
$table->boolean('nama') | True / false |
$table->foreignId('user_id')->constrained() | Foreign key ke tabel lain |
$table->timestamps() | Kolom created_at & updated_at otomatis |
Eloquent ORM (Model)
Eloquent adalah ORM Laravel โ satu Model mewakili satu tabel. Dengan Eloquent, query database ditulis dengan gaya PHP yang ekspresif, bukan SQL mentah.
php artisan make:model Product -m
# -m otomatis membuatkan file migration sekaligusclass Product extends Model
{
protected $fillable = ['name', 'description', 'price', 'stock', 'photo'];
}$fillable adalah daftar kolom yang boleh diisi lewat create() atau update() secara massal โ ini penting untuk keamanan (mencegah mass assignment ke kolom yang tidak seharusnya diubah user).
Operasi dasar (CRUD via Eloquent)
// Create
Product::create(['name' => 'Kaos', 'price' => 50000]);
// Read
Product::all();
Product::find(1);
Product::where('price', '>', 100000)->get();
// Update
$product = Product::find(1);
$product->update(['price' => 60000]);
// Delete
Product::find(1)->delete();Query Builder
Kalau butuh query yang lebih fleksibel tanpa Model, Laravel punya Query Builder lewat facade DB.
use Illuminate\Support\Facades\DB;
$products = DB::table('products')
->where('stock', '>', 0)
->orderBy('price', 'desc')
->limit(10)
->get();Kombinasi kondisi yang sering dipakai
Product::where('category', 'baju')
->orWhere('category', 'celana')
->whereBetween('price', [10000, 100000])
->orderBy('name')
->paginate(15);
// pencarian sederhana
Product::where('name', 'like', '%'.$keyword.'%')->get();Seeder & Factory
Seeder dipakai untuk mengisi data awal/dummy ke database, misalnya saat development supaya tidak perlu input manual satu-satu.
php artisan make:seeder ProductSeeder
php artisan make:factory ProductFactory --model=Productpublic function definition(): array
{
return [
'name' => fake()->words(3, true),
'price' => fake()->numberBetween(10000, 500000),
'stock' => fake()->numberBetween(0, 100),
];
}public function run(): void
{
Product::factory()->count(20)->create();
}php artisan db:seed --class=ProductSeederRelasi Eloquent
Relasi menghubungkan antar tabel โ misalnya satu Kategori punya banyak Produk.
One to Many
public function products()
{
return $this->hasMany(Product::class);
}public function category()
{
return $this->belongsTo(Category::class);
}$category->products; // semua produk milik kategori ini
$product->category->name; // nama kategori dari sebuah produkMany to Many
// contoh: Product <-> Tag (butuh tabel pivot product_tag)
public function tags()
{
return $this->belongsToMany(Tag::class);
}Product::with('category')->get() agar relasi diambil sekaligus, menghindari masalah N+1 query yang bikin aplikasi lambat.Autentikasi (Login & Register)
Cara tercepat menambahkan sistem login di Laravel 13 adalah lewat paket resmi Laravel Breeze.
composer require laravel/breeze --dev
php artisan breeze:install blade
npm install && npm run build
php artisan migratePerintah di atas otomatis membuatkan halaman login, register, reset password, beserta route dan Controller-nya.
Melindungi route dengan middleware auth
Route::middleware('auth')->group(function () {
Route::resource('produk', ProductController::class);
});Mengecek user yang sedang login
// di Controller
$user = auth()->user();
// di Blade
@auth
Halo, {{ auth()->user()->name }}
@endauthUpload File
<form action="{{ route('products.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<input type="file" name="photo">
</form>if ($request->hasFile('photo')) {
$path = $request->file('photo')->store('products', 'public');
$product->photo = $path;
$product->save();
}Agar file bisa diakses lewat browser, buat symbolic link sekali saja:
php artisan storage:linkLalu tampilkan gambar dengan: <img src="{{ asset('storage/' . $product->photo) }}">
Session & Flash Message
Flash message adalah pesan sementara (biasanya notifikasi sukses/gagal) yang hanya tampil satu kali setelah redirect.
return redirect()->route('products.index')
->with('success', 'Data berhasil disimpan!');@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endifAPI Resource
Kalau aplikasi kamu juga perlu diakses lewat API (misalnya untuk aplikasi mobile), gunakan API Resource agar output JSON rapi dan konsisten.
php artisan make:resource ProductResourcepublic function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
];
}Route::apiResource('products', ProductApiController::class);Artisan Command
artisan adalah CLI bawaan Laravel. Beberapa perintah yang paling sering dipakai:
| Perintah | Fungsi |
|---|---|
php artisan make:model Nama -mcr | Buat Model + migration + controller resource sekaligus |
php artisan route:list | Lihat semua route terdaftar |
php artisan migrate:rollback | Batalkan migration terakhir |
php artisan tinker | Konsol interaktif untuk mencoba kode Laravel |
php artisan optimize:clear | Bersihkan semua cache (config, route, view) |
php artisan make:command NamaCommand | Buat perintah artisan custom sendiri |
Pengenalan Livewire 4
Livewire memungkinkan kita membuat komponen yang reaktif โ berinteraksi dengan server tanpa menulis JavaScript dan tanpa reload halaman, tapi tetap ditulis dengan PHP & Blade seperti biasa.
composer require livewire/livewirePastikan direktif berikut ada di layout utama (biasanya sebelum </head> dan sebelum </body>):
<head>
...
@livewireStyles
</head>
<body>
...
@livewireScripts
</body>Konsep dasar
- Component class โ file PHP di
app/Livewireyang menyimpan data (properti publik) dan aksi (method publik). - View โ file Blade di
resources/views/livewireyang otomatis ter-update saat data berubah. - wire:model โ mengikat input form langsung ke properti PHP secara dua-arah.
- wire:click โ memanggil method PHP saat tombol diklik, tanpa reload.
php artisan make:livewire Counterclass Counter extends Component
{
public int $count = 0;
public function tambah()
{
$this->count++;
}
public function render()
{
return view('livewire.counter');
}
}<div>
<h3>Jumlah: {{ $count }}</h3>
<button wire:click="tambah" class="btn btn-primary">Tambah</button>
</div>Panggil komponen ini di halaman Blade manapun dengan <livewire:counter />.
Studi Kasus 1 โ CRUD Laravel 13 + Blade + Bootstrap
Kita akan membuat CRUD sederhana untuk data Produk (nama, harga, stok) memakai cara klasik: Controller + Blade + reload halaman biasa, dengan tampilan Bootstrap 5.
Buat Model, Migration, dan Controller sekaligus
php artisan make:model Product -mcrIsi migration
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->decimal('price', 10, 2);
$table->integer('stock')->default(0);
$table->timestamps();
});
}php artisan migrateModel โ izinkan mass assignment
class Product extends Model
{
protected $fillable = ['name', 'price', 'stock'];
}Daftarkan resource route
use App\Http\Controllers\ProductController;
Route::resource('products', ProductController::class);Controller lengkap (7 method)
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index()
{
$products = Product::latest()->paginate(8);
return view('products.index', compact('products'));
}
public function create()
{
return view('products.create');
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
]);
Product::create($request->all());
return redirect()->route('products.index')
->with('success', 'Produk berhasil ditambahkan.');
}
public function edit(Product $product)
{
return view('products.edit', compact('product'));
}
public function update(Request $request, Product $product)
{
$request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
]);
$product->update($request->all());
return redirect()->route('products.index')
->with('success', 'Produk berhasil diperbarui.');
}
public function destroy(Product $product)
{
$product->delete();
return redirect()->route('products.index')
->with('success', 'Produk berhasil dihapus.');
}
}Product $product pada method edit/update/destroy โ Laravel otomatis mengambil data dari ID di URL, tanpa perlu Product::find() manual.Layout dasar dengan Bootstrap 5
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>@yield('title', 'Manajemen Produk')</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
@yield('content')
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>Halaman daftar produk (index)
@extends('layouts.app')
@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="m-0">Daftar Produk</h2>
<a href="{{ route('products.create') }}" class="btn btn-primary">+ Tambah Produk</a>
</div>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
<table class="table table-bordered bg-white">
<thead class="table-dark">
<tr>
<th>#</th>
<th>Nama</th>
<th>Harga</th>
<th>Stok</th>
<th class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($products as $product)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $product->name }}</td>
<td>Rp{{ number_format($product->price, 0, ',', '.') }}</td>
<td>{{ $product->stock }}</td>
<td class="text-center">
<a href="{{ route('products.edit', $product) }}" class="btn btn-sm btn-warning">Edit</a>
<form action="{{ route('products.destroy', $product) }}" method="POST" class="d-inline"
onsubmit="return confirm('Yakin hapus produk ini?')">
@csrf
@method('DELETE')
<button class="btn btn-sm btn-danger">Hapus</button>
</form>
</td>
</tr>
@empty
<tr><td colspan="5" class="text-center text-muted">Belum ada produk</td></tr>
@endforelse
</tbody>
</table>
{{ $products->links() }}
@endsectionForm tambah (create)
@extends('layouts.app')
@section('content')
<h2 class="mb-4">Tambah Produk</h2>
<form action="{{ route('products.store') }}" method="POST" class="bg-white p-4 rounded shadow-sm">
@csrf
<div class="mb-3">
<label class="form-label">Nama Produk</label>
<input type="text" name="name" class="form-control @error('name') is-invalid @enderror" value="{{ old('name') }}">
@error('name') <div class="invalid-feedback">{{ $message }}</div> @enderror
</div>
<div class="mb-3">
<label class="form-label">Harga</label>
<input type="number" name="price" class="form-control @error('price') is-invalid @enderror" value="{{ old('price') }}">
@error('price') <div class="invalid-feedback">{{ $message }}</div> @enderror
</div>
<div class="mb-3">
<label class="form-label">Stok</label>
<input type="number" name="stock" class="form-control @error('stock') is-invalid @enderror" value="{{ old('stock') }}">
@error('stock') <div class="invalid-feedback">{{ $message }}</div> @enderror
</div>
<a href="{{ route('products.index') }}" class="btn btn-secondary">Batal</a>
<button class="btn btn-primary">Simpan</button>
</form>
@endsectionForm ubah (edit) โ mirip create, tinggal tambah @method('PUT')
@extends('layouts.app')
@section('content')
<h2 class="mb-4">Edit Produk</h2>
<form action="{{ route('products.update', $product) }}" method="POST" class="bg-white p-4 rounded shadow-sm">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label">Nama Produk</label>
<input type="text" name="name" class="form-control" value="{{ old('name', $product->name) }}">
</div>
<div class="mb-3">
<label class="form-label">Harga</label>
<input type="number" name="price" class="form-control" value="{{ old('price', $product->price) }}">
</div>
<div class="mb-3">
<label class="form-label">Stok</label>
<input type="number" name="stock" class="form-control" value="{{ old('stock', $product->stock) }}">
</div>
<a href="{{ route('products.index') }}" class="btn btn-secondary">Batal</a>
<button class="btn btn-primary">Update</button>
</form>
@endsection/products โ kamu sudah punya CRUD penuh: tambah, lihat daftar (dengan paginasi), ubah, dan hapus data, lengkap dengan validasi dan notifikasi sukses.Studi Kasus 2 โ CRUD Laravel 13 + Livewire 4 + Bootstrap
Sekarang kita bangun CRUD Produk yang sama, tapi versi reaktif: tambah, edit, dan hapus data langsung dari satu halaman tanpa reload, memakai Livewire 4. Model dan migration memakai yang sudah dibuat di studi kasus sebelumnya.
Install Livewire (jika belum)
composer require livewire/livewireBuat komponen Livewire
php artisan make:livewire ProductManagerIsi class komponen โ semua logika CRUD ada di sini
namespace App\Livewire;
use App\Models\Product;
use Livewire\Component;
use Livewire\WithPagination;
class ProductManager extends Component
{
use WithPagination;
public $name, $price, $stock;
public $productId;
public bool $isEdit = false;
public bool $showModal = false;
protected function rules()
{
return [
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
];
}
public function render()
{
return view('livewire.product-manager', [
'products' => Product::latest()->paginate(8),
]);
}
public function openCreate()
{
$this->resetForm();
$this->isEdit = false;
$this->showModal = true;
}
public function openEdit($id)
{
$product = Product::findOrFail($id);
$this->productId = $product->id;
$this->name = $product->name;
$this->price = $product->price;
$this->stock = $product->stock;
$this->isEdit = true;
$this->showModal = true;
}
public function save()
{
$this->validate();
Product::updateOrCreate(
['id' => $this->productId],
['name' => $this->name, 'price' => $this->price, 'stock' => $this->stock]
);
session()->flash('success', $this->isEdit ? 'Produk berhasil diperbarui.' : 'Produk berhasil ditambahkan.');
$this->showModal = false;
$this->resetForm();
}
public function delete($id)
{
Product::findOrFail($id)->delete();
session()->flash('success', 'Produk berhasil dihapus.');
}
private function resetForm()
{
$this->reset(['productId', 'name', 'price', 'stock']);
$this->resetErrorBag();
}
}wire:click atau wire:model dipicu, Livewire mengirim request kecil ke server di belakang layar, menjalankan method PHP terkait, lalu hanya bagian HTML yang berubah yang di-render ulang di browser.View komponen โ tabel + modal form, semua dalam satu file
<div>
@if (session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="m-0">Manajemen Produk <small class="text-muted">(Livewire)</small></h2>
<button wire:click="openCreate" class="btn btn-primary">+ Tambah Produk</button>
</div>
<table class="table table-bordered bg-white">
<thead class="table-dark">
<tr>
<th>#</th><th>Nama</th><th>Harga</th><th>Stok</th><th class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($products as $product)
<tr wire:key="product-{{ $product->id }}">
<td>{{ $loop->iteration }}</td>
<td>{{ $product->name }}</td>
<td>Rp{{ number_format($product->price, 0, ',', '.') }}</td>
<td>{{ $product->stock }}</td>
<td class="text-center">
<button wire:click="openEdit({{ $product->id }})" class="btn btn-sm btn-warning">Edit</button>
<button wire:click="delete({{ $product->id }})"
wire:confirm="Yakin hapus produk ini?"
class="btn btn-sm btn-danger">Hapus</button>
</td>
</tr>
@empty
<tr><td colspan="5" class="text-center text-muted">Belum ada produk</td></tr>
@endforelse
</tbody>
</table>
{{ $products->links() }}
{{-- Modal form tambah / edit --}}
@if($showModal)
<div class="modal d-block" style="background:rgba(0,0,0,.5)" wire:click.self="$set('showModal', false)">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $isEdit ? 'Edit Produk' : 'Tambah Produk' }}</h5>
<button type="button" class="btn-close" wire:click="$set('showModal', false)"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Nama Produk</label>
<input type="text" wire:model="name" class="form-control @error('name') is-invalid @enderror">
@error('name') <div class="invalid-feedback">{{ $message }}</div> @enderror
</div>
<div class="mb-3">
<label class="form-label">Harga</label>
<input type="number" wire:model="price" class="form-control @error('price') is-invalid @enderror">
@error('price') <div class="invalid-feedback">{{ $message }}</div> @enderror
</div>
<div class="mb-3">
<label class="form-label">Stok</label>
<input type="number" wire:model="stock" class="form-control @error('stock') is-invalid @enderror">
@error('stock') <div class="invalid-feedback">{{ $message }}</div> @enderror
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" wire:click="$set('showModal', false)">Batal</button>
<button class="btn btn-primary" wire:click="save">Simpan</button>
</div>
</div>
</div>
</div>
@endif
</div>$name, $price, $stock โ begitu tombol Simpan diklik, method save() membaca nilai-nilai itu langsung tanpa perlu $request->input().Pasang komponen di halaman & pastikan directive Livewire ada
Route::get('/produk-livewire', function () {
return view('produk-livewire');
});@extends('layouts.app')
@section('content')
<livewire:product-manager />
@endsectionPastikan @livewireStyles dan @livewireScripts sudah ada di layouts/app.blade.php seperti dibahas di bagian 19 โ kalau belum, tambahkan dulu supaya interaktivitas Livewire berfungsi.
/produk-livewire menampilkan tabel produk dengan tombol Tambah/Edit/Hapus yang membuka modal dan menyimpan data secara real-time, tanpa satu pun reload halaman penuh.