Pendahuluan
Laravel 13
๐Ÿ“˜ Panduan Belajar dari Nol

Belajar Laravel 13, dari Instalasi sampai Bikin CRUD Sungguhan

Materi ini disusun berurutan seperti buku: mulai dari konsep dasar framework, routing, database, sampai dua praktik CRUD lengkap โ€” versi Blade + Bootstrap klasik, dan versi Livewire 4 + Bootstrap yang reaktif tanpa reload halaman.

21 topik
2 studi kasus CRUD penuh
PHP 8.3+ ยท MySQL ยท Bootstrap 5
01 / Pengenalan

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.

Catatan: Materi ini mengasumsikan kamu sudah mengerti dasar PHP (variabel, function, class, array) dan pernah dengar istilah "database". Tidak perlu jago, cukup familiar.
02 / Pengenalan

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

1

Install lewat Composer

terminal
# 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-project
2

Masuk ke folder project & jalankan server

terminal
cd nama-project
php artisan serve

Buka http://127.0.0.1:8000 di browser โ€” kalau muncul halaman selamat datang Laravel, instalasi berhasil.

3

Atur koneksi database di file .env

.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:

terminal
php artisan migrate
Perhatian: Setiap kali mengubah isi file .env, jalankan php artisan config:clear agar perubahan langsung terbaca.
03 / Pengenalan

Struktur Folder Project

Laravel 13 memakai struktur ramping ala Laravel 11+. Berikut folder dan file yang paling sering kamu sentuh sehari-hari:

PathFungsi
app/Http/ControllersSemua Controller diletakkan di sini
app/ModelsSemua Model Eloquent
app/LivewireKomponen Livewire (dibuat otomatis saat Livewire di-install)
routes/web.phpDaftar route untuk halaman web
routes/api.phpDaftar route untuk API (perlu diaktifkan dulu)
resources/viewsFile Blade (.blade.php) โ€” tampilan HTML
database/migrationsStruktur tabel database, versi-terkontrol
database/seedersData awal / dummy untuk mengisi tabel
publicFolder yang diakses browser (berisi index.php, aset publik)
bootstrap/app.phpTitik konfigurasi utama aplikasi (routing, middleware, exception)
.envVariabel environment (koneksi database, mail, dsb)
Perbedaan dengan versi lama: Di Laravel versi sebelum 11, ada file app/Http/Kernel.php untuk daftar middleware. Di Laravel 13, semua itu didaftarkan langsung di bootstrap/app.php.
04 / Dasar-dasar

Routing

Route adalah pintu masuk: memetakan sebuah URL ke logika yang harus dijalankan. Semua route web didefinisikan di routes/web.php.

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

MethodDipakai 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:

VerbURIMethod ControllerFungsi
GET/produkindexDaftar semua data
GET/produk/createcreateForm tambah data
POST/produkstoreSimpan data baru
GET/produk/{id}showDetail satu data
GET/produk/{id}/editeditForm ubah data
PUT/PATCH/produk/{id}updateSimpan perubahan
DELETE/produk/{id}destroyHapus data
05 / Dasar-dasar

Controller

Controller menampung logika aplikasi, supaya route tetap ringkas. Buat controller lewat Artisan:

terminal
php artisan make:controller ProductController --resource

Opsi --resource otomatis membuatkan 7 method kosong (index, create, store, show, edit, update, destroy) sesuai konvensi resource route.

app/Http/Controllers/ProductController.php
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.');
    }
}
Best practice: Controller sebaiknya tipis โ€” cukup memanggil Model dan mengembalikan view/response. Logika bisnis yang kompleks lebih baik dipindah ke Service class atau Model.
06 / Dasar-dasar

Blade Templating

Blade adalah mesin template Laravel. File Blade berakhiran .blade.php dan disimpan di resources/views.

Sintaks dasar

Blade
{{-- 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 }}
  • @endforeach

    Layout & pewarisan template

    Blade memakai konsep layout induk yang bisa "diwariskan" oleh halaman lain, supaya header/footer tidak ditulis berulang.

    resources/views/layouts/app.blade.php
    <!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>
    resources/views/products/index.blade.php
    @extends('layouts.app')
    
    @section('title', 'Daftar Produk')
    
    @section('content')
        <h1>Daftar Produk</h1>
    @endsection

    Komponen Blade

    Untuk elemen yang dipakai berulang (tombol, card, alert), gunakan komponen:

    terminal
    php artisan make:component Alert
    penggunaan di Blade
    <x-alert type="success">Data berhasil disimpan</x-alert>
    07 / Dasar-dasar

    Middleware

    Middleware adalah lapisan penyaring yang dijalankan sebelum (atau sesudah) request sampai ke Controller โ€” misalnya untuk mengecek apakah user sudah login.

    terminal
    php artisan make:middleware CekUmur
    app/Http/Middleware/CekUmur.php
    public function handle(Request $request, Closure $next)
    {
        if ($request->age < 18) {
            return redirect('/');
        }
    
        return $next($request);
    }

    Daftarkan alias middleware di bootstrap/app.php:

    bootstrap/app.php
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->alias([
            'cek.umur' => \App\Http\Middleware\CekUmur::class,
        ]);
    })

    Lalu pakai di route:

    routes/web.php
    Route::get('/khusus-dewasa', [PageController::class, 'index'])
        ->middleware('cek.umur');
    Middleware bawaan yang sering dipakai: auth (harus login), guest (harus belum login), throttle (batasi jumlah request).
    08 / Dasar-dasar

    Request & Validasi

    Semua data yang dikirim user (lewat form atau URL) bisa diakses lewat object Request.

    Controller
    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.

    Controller
    $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:

    Blade
    @error('name')
        <div class="text-danger">{{ $message }}</div>
    @enderror

    Form Request (validasi terpisah, untuk form yang kompleks)

    terminal
    php artisan make:request StoreProductRequest

    Lalu di Controller cukup ganti tipe parameter menjadi StoreProductRequest $request โ€” Laravel otomatis menjalankan validasinya sebelum masuk ke method.

    09 / Database

    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.

    terminal
    php artisan make:migration create_products_table
    database/migrations/xxxx_create_products_table.php
    public 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');
    }
    terminal
    # jalankan semua migration yang belum dijalankan
    php artisan migrate
    
    # hapus semua tabel lalu jalankan ulang dari awal
    php artisan migrate:fresh
    Tipe kolomKeterangan
    $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
    10 / Database

    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.

    terminal
    php artisan make:model Product -m
    # -m otomatis membuatkan file migration sekaligus
    app/Models/Product.php
    class 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)

    Contoh penggunaan
    // 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();
    11 / Database

    Query Builder

    Kalau butuh query yang lebih fleksibel tanpa Model, Laravel punya Query Builder lewat facade DB.

    Contoh
    use Illuminate\Support\Facades\DB;
    
    $products = DB::table('products')
        ->where('stock', '>', 0)
        ->orderBy('price', 'desc')
        ->limit(10)
        ->get();

    Kombinasi kondisi yang sering dipakai

    Contoh
    Product::where('category', 'baju')
        ->orWhere('category', 'celana')
        ->whereBetween('price', [10000, 100000])
        ->orderBy('name')
        ->paginate(15);
    
    // pencarian sederhana
    Product::where('name', 'like', '%'.$keyword.'%')->get();
    12 / Database

    Seeder & Factory

    Seeder dipakai untuk mengisi data awal/dummy ke database, misalnya saat development supaya tidak perlu input manual satu-satu.

    terminal
    php artisan make:seeder ProductSeeder
    php artisan make:factory ProductFactory --model=Product
    database/factories/ProductFactory.php
    public function definition(): array
    {
        return [
            'name'  => fake()->words(3, true),
            'price' => fake()->numberBetween(10000, 500000),
            'stock' => fake()->numberBetween(0, 100),
        ];
    }
    database/seeders/ProductSeeder.php
    public function run(): void
    {
        Product::factory()->count(20)->create();
    }
    terminal
    php artisan db:seed --class=ProductSeeder
    13 / Database

    Relasi Eloquent

    Relasi menghubungkan antar tabel โ€” misalnya satu Kategori punya banyak Produk.

    One to Many

    app/Models/Category.php
    public function products()
    {
        return $this->hasMany(Product::class);
    }
    app/Models/Product.php
    public function category()
    {
        return $this->belongsTo(Category::class);
    }
    Penggunaan
    $category->products;      // semua produk milik kategori ini
    $product->category->name; // nama kategori dari sebuah produk

    Many to Many

    Model
    // contoh: Product <-> Tag (butuh tabel pivot product_tag)
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
    Eager loading: gunakan Product::with('category')->get() agar relasi diambil sekaligus, menghindari masalah N+1 query yang bikin aplikasi lambat.
    14 / Fitur Lanjutan

    Autentikasi (Login & Register)

    Cara tercepat menambahkan sistem login di Laravel 13 adalah lewat paket resmi Laravel Breeze.

    terminal
    composer require laravel/breeze --dev
    php artisan breeze:install blade
    npm install && npm run build
    php artisan migrate

    Perintah di atas otomatis membuatkan halaman login, register, reset password, beserta route dan Controller-nya.

    Melindungi route dengan middleware auth

    routes/web.php
    Route::middleware('auth')->group(function () {
        Route::resource('produk', ProductController::class);
    });

    Mengecek user yang sedang login

    Blade / Controller
    // di Controller
    $user = auth()->user();
    
    // di Blade
    @auth
        Halo, {{ auth()->user()->name }}
    @endauth
    15 / Fitur Lanjutan

    Upload File

    Blade โ€” form harus punya enctype
    <form action="{{ route('products.store') }}" method="POST" enctype="multipart/form-data">
        @csrf
        <input type="file" name="photo">
    </form>
    Controller
    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:

    terminal
    php artisan storage:link

    Lalu tampilkan gambar dengan: <img src="{{ asset('storage/' . $product->photo) }}">

    16 / Fitur Lanjutan

    Session & Flash Message

    Flash message adalah pesan sementara (biasanya notifikasi sukses/gagal) yang hanya tampil satu kali setelah redirect.

    Controller
    return redirect()->route('products.index')
        ->with('success', 'Data berhasil disimpan!');
    Blade
    @if(session('success'))
        <div class="alert alert-success">{{ session('success') }}</div>
    @endif
    17 / Fitur Lanjutan

    API Resource

    Kalau aplikasi kamu juga perlu diakses lewat API (misalnya untuk aplikasi mobile), gunakan API Resource agar output JSON rapi dan konsisten.

    terminal
    php artisan make:resource ProductResource
    app/Http/Resources/ProductResource.php
    public function toArray(Request $request): array
    {
        return [
            'id'    => $this->id,
            'name'  => $this->name,
            'price' => $this->price,
        ];
    }
    routes/api.php
    Route::apiResource('products', ProductApiController::class);
    18 / Fitur Lanjutan

    Artisan Command

    artisan adalah CLI bawaan Laravel. Beberapa perintah yang paling sering dipakai:

    PerintahFungsi
    php artisan make:model Nama -mcrBuat Model + migration + controller resource sekaligus
    php artisan route:listLihat semua route terdaftar
    php artisan migrate:rollbackBatalkan migration terakhir
    php artisan tinkerKonsol interaktif untuk mencoba kode Laravel
    php artisan optimize:clearBersihkan semua cache (config, route, view)
    php artisan make:command NamaCommandBuat perintah artisan custom sendiri
    19 / Livewire 4

    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.

    terminal โ€” instalasi
    composer require livewire/livewire

    Pastikan direktif berikut ada di layout utama (biasanya sebelum </head> dan sebelum </body>):

    resources/views/layouts/app.blade.php
    <head>
        ...
        @livewireStyles
    </head>
    <body>
        ...
        @livewireScripts
    </body>

    Konsep dasar

    • Component class โ€” file PHP di app/Livewire yang menyimpan data (properti publik) dan aksi (method publik).
    • View โ€” file Blade di resources/views/livewire yang 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.
    terminal โ€” membuat komponen
    php artisan make:livewire Counter
    app/Livewire/Counter.php
    class Counter extends Component
    {
        public int $count = 0;
    
        public function tambah()
        {
            $this->count++;
        }
    
        public function render()
        {
            return view('livewire.counter');
        }
    }
    resources/views/livewire/counter.blade.php
    <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 />.

    Livewire 4: membawa performa render yang lebih cepat, dukungan Islands untuk memuat sebagian komponen secara independen, dan penulisan komponen single-file yang lebih ringkas โ€” namun konsep intinya (properti publik + wire:model + wire:click) tetap sama seperti versi sebelumnya, sehingga materi CRUD berikut tetap berlaku.
    20 / Praktik CRUD

    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.

    1

    Buat Model, Migration, dan Controller sekaligus

    terminal
    php artisan make:model Product -mcr
    2

    Isi migration

    database/migrations/xxxx_create_products_table.php
    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();
        });
    }
    terminal
    php artisan migrate
    3

    Model โ€” izinkan mass assignment

    app/Models/Product.php
    class Product extends Model
    {
        protected $fillable = ['name', 'price', 'stock'];
    }
    4

    Daftarkan resource route

    routes/web.php
    use App\Http\Controllers\ProductController;
    
    Route::resource('products', ProductController::class);
    5

    Controller lengkap (7 method)

    app/Http/Controllers/ProductController.php
    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.');
        }
    }
    Route Model Binding: perhatikan parameter Product $product pada method edit/update/destroy โ€” Laravel otomatis mengambil data dari ID di URL, tanpa perlu Product::find() manual.
    6

    Layout dasar dengan Bootstrap 5

    resources/views/layouts/app.blade.php
    <!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>
    7

    Halaman daftar produk (index)

    resources/views/products/index.blade.php
    @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() }}
    @endsection
    8

    Form tambah (create)

    resources/views/products/create.blade.php
    @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>
    @endsection
    9

    Form ubah (edit) โ€” mirip create, tinggal tambah @method('PUT')

    resources/views/products/edit.blade.php
    @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
    Selesai! Buka /products โ€” kamu sudah punya CRUD penuh: tambah, lihat daftar (dengan paginasi), ubah, dan hapus data, lengkap dengan validasi dan notifikasi sukses.
    21 / Praktik CRUD

    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.

    1

    Install Livewire (jika belum)

    terminal
    composer require livewire/livewire
    2

    Buat komponen Livewire

    terminal
    php artisan make:livewire ProductManager
    3

    Isi class komponen โ€” semua logika CRUD ada di sini

    app/Livewire/ProductManager.php
    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();
        }
    }
    Kenapa tanpa reload? Setiap kali 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.
    4

    View komponen โ€” tabel + modal form, semua dalam satu file

    resources/views/livewire/product-manager.blade.php
    <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>
    wire:model di setiap input otomatis menyambungkan nilai input ke properti PHP $name, $price, $stock โ€” begitu tombol Simpan diklik, method save() membaca nilai-nilai itu langsung tanpa perlu $request->input().
    5

    Pasang komponen di halaman & pastikan directive Livewire ada

    routes/web.php
    Route::get('/produk-livewire', function () {
        return view('produk-livewire');
    });
    resources/views/produk-livewire.blade.php
    @extends('layouts.app')
    
    @section('content')
        <livewire:product-manager />
    @endsection

    Pastikan @livewireStyles dan @livewireScripts sudah ada di layouts/app.blade.php seperti dibahas di bagian 19 โ€” kalau belum, tambahkan dulu supaya interaktivitas Livewire berfungsi.

    Hasil akhir: membuka /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.

    Selesai โ€” sekarang giliranmu praktik

    Materi ini paling efektif kalau langsung dipraktikkan sambil ngoding di project sendiri. Coba mulai dari instalasi (bagian 02), lalu kerjakan dua studi kasus CRUD di akhir sebagai latihan utama.