Materi 1 · Modul 0
Kenalan sama Laravel
Framework PHP paling populer — dan kenapa dia bisa menghemat begitu banyak waktu dibanding menulis semuanya manual.
Kenapa perlu framework
Di handbook PHP Materi 25, kamu membangun sendiri routing sederhana, struktur folder, dan pemisahan controller/view. Itu bagus untuk belajar — tapi setiap project baru, kamu harus membangun ulang: sistem autentikasi, validasi, koneksi database yang rapi, proteksi CSRF, dan puluhan hal lain yang hampir selalu dibutuhkan aplikasi web.
Laravel adalah framework yang sudah menyediakan semua itu, teruji oleh jutaan project, dan dirancang supaya potongan-potongannya saling cocok dengan rapi. Kamu tidak lagi membangun fondasi dari nol — kamu langsung membangun fitur.
Apa yang Laravel sediakan
| Kebutuhan | Yang kamu tulis manual (handbook PHP) | Yang Laravel sediakan |
|---|---|---|
| Routing | Array route + regex manual | File route yang deklaratif, rapi |
| Database | PDO + query manual | Eloquent ORM — objek yang "berbicara" ke database |
| Validasi | Class Validator sendiri | Sistem validasi bawaan dengan puluhan aturan siap pakai |
| Template | PHP dicampur HTML manual | Blade — template engine dengan layout & component |
| Autentikasi | Session + password_hash manual | Sistem login/register siap pakai (Breeze, dibahas Modul 3) |
| Keamanan | CSRF token manual | Otomatis aktif di semua form |
Catatan — Laravel tidak mengganti apa yang kamu pelajari di handbook PHP dan MySQL — dia dibangun di atas semua itu. Setiap kali Laravel "melakukan sesuatu secara otomatis" (menangani request, query ke database, memvalidasi input), di baliknya tetap PHP murni dan SQL yang bekerja — cuma sudah dibungkus rapi supaya kamu tidak perlu menulis ulang setiap kali.
Yang akan kamu pelajari
- Modul 0 — instalasi, routing, Blade dasar, Artisan CLI.
- Modul 1 — database lewat Eloquent: migration, model, relasi, seeder.
- Modul 2 — MVC praktis: controller, form request, layout, dan studi kasus CRUD lengkap.
- Modul 3 — autentikasi, middleware, otorisasi, upload file, keamanan.
- Modul 4 — membangun API, queue, event, testing.
- Modul 5 — caching dan praktik produksi.
Prasyarat — Handbook ini mengasumsikan kamu sudah menyelesaikan PHP dari Nol (terutama OOP di Modul 3, dan Composer/namespace di Modul 4) dan MySQL dari Nol (terutama relasi dan desain database di Modul 3). Laravel memakai OOP secara intensif — kalau class, interface, dan inheritance masih terasa asing, sebaiknya kembali dulu ke handbook PHP.
Materi 2 · Modul 0
Instalasi dan struktur project
Menyiapkan project Laravel pertama, dan memahami untuk apa tiap foldernya.
Instalasi
# pastikan PHP 8.2+ dan Composer sudah terpasang (handbook PHP Materi 21)
composer create-project laravel/laravel toko-laravel
# masuk folder project, jalankan server development
cd toko-laravel
php artisan serve
# buka http://localhost:8000Catatan — artisan adalah command-line tool bawaan Laravel — akan kamu pakai terus-menerus untuk membuat file, menjalankan migration, dan banyak lagi. Dibahas mendalam di Materi 5. Untuk sekarang, cukup tahu: php artisan serve menjalankan server development, mirip php -S localhost:8000 yang kamu pakai di handbook PHP, tapi dengan beberapa penyesuaian khusus Laravel.
Struktur folder
toko-laravel/ ├── app/ │ ├── Http/ │ │ ├── Controllers/ ← logika yang menangani request │ │ ├── Middleware/ ← penyaring request (Materi 18) │ │ └── Requests/ ← validasi form (Materi 13) │ ├── Models/ ← Eloquent model (Materi 7) │ └── Providers/ ├── bootstrap/ ├── config/ ← file konfigurasi per fitur (database.php, mail.php, dll) ├── database/ │ ├── migrations/ ← struktur tabel sebagai kode (Materi 6) │ ├── seeders/ ← data awal/dummy (Materi 10) │ └── factories/ ← generator data palsu untuk testing (Materi 10) ├── public/ ← document root — SATU-SATUNYA folder yang diakses browser ├── resources/ │ └── views/ ← file Blade (Materi 4) ├── routes/ │ ├── web.php ← route untuk halaman web │ └── api.php ← route untuk API (Materi 22) ├── storage/ ← file upload, log, cache ├── tests/ ← test otomatis (Materi 26) ├── .env ← konfigurasi rahasia (mirip Materi 24 handbook PHP) └── artisan ← entry point command-line
Catatan — Perhatikan kemiripannya dengan struktur yang kamu bangun manual di handbook PHP Materi 25 dan studi kasus: ada public/ sebagai satu-satunya pintu masuk, ada pemisahan Controller/Model/View, ada file .env. Laravel bukan konsep baru — dia versi matang dan lengkap dari pola yang sudah kamu pahami.
File .env
APP_NAME="Toko Laravel"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=toko_laravel
DB_USERNAME=root
DB_PASSWORD=Laravel otomatis memakai library vlucas/phpdotenv (yang kamu pasang manual di handbook PHP Materi 24) — jadi kamu tidak perlu memasangnya sendiri. Buat database toko_laravel di MySQL lebih dulu (lewat CREATE DATABASE, seperti Materi 3 handbook MySQL) sebelum lanjut ke migration di Materi 6.
Halaman pertama
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return 'Halo dari Laravel!';
});Buka http://localhost:8000 — kalau muncul teks itu, project sudah jalan. Materi berikutnya membahas routing lebih dalam.
Latihan — Instal project Laravel baru, buat database MySQL kosong untuknya, sesuaikan .env, dan pastikan php artisan serve berjalan tanpa error. Ubah teks di route / jadi sesuatu yang lain, refresh browser, pastikan perubahan langsung terlihat.
Materi 3 · Modul 0
Routing
Menentukan URL apa yang tersedia, dan apa yang terjadi saat diakses — versi rapi dari router manual di handbook PHP.
Route dasar
Route::get('/produk', function () {
return 'Daftar produk';
});
Route::post('/produk', function () {
return 'Simpan produk baru';
});
Route::put('/produk/{id}', function () { /* update */ });
Route::delete('/produk/{id}', function () { /* hapus */ });Catatan — Ini persis konsep $routes yang kamu tulis manual di studi kasus PHP+MySQL (Materi 4), tapi Laravel mendukung semua HTTP method (GET, POST, PUT, DELETE) secara native, tanpa kamu perlu menyimulasikan lewat _method di form sendiri.
Parameter route
// {id} ditangkap otomatis, TANPA regex manual seperti di studi kasus PHP Materi 11
Route::get('/produk/{id}', function (string $id) {
return "Detail produk #{$id}";
});
// parameter opsional — tanda ? dan nilai default di parameter fungsi
Route::get('/produk/{id?}', function (?string $id = null) {
return $id ? "Produk #{$id}" : "Semua produk";
});
// batasi format parameter dengan constraint
Route::get('/produk/{id}', function (string $id) { /* ... */ })
->where('id', '[0-9]+'); // hanya angkaRoute menuju controller
Menulis logika langsung di routes/web.php cepat untuk contoh kecil, tapi untuk aplikasi sungguhan, logika dipindah ke controller (dibahas mendalam Materi 12). Sekilas bentuknya:
use App\Http\Controllers\ProdukController;
Route::get('/produk', [ProdukController::class, 'index']);
Route::get('/produk/{id}', [ProdukController::class, 'show']);Named route: memberi nama, bukan hardcode URL
Route::get('/produk', [ProdukController::class, 'index'])->name('produk.index');
Route::get('/produk/{id}', [ProdukController::class, 'show'])->name('produk.show');<!-- alih-alih hardcode "/produk", pakai nama route -->
<a href="{{ route('produk.index') }}">Lihat Produk</a>
<a href="{{ route('produk.show', $produk->id) }}">Detail</a>Hati-hati — Selalu pakai named route (route('produk.index')) daripada menulis URL langsung (href="/produk") di view. Kalau suatu saat URL-nya diubah (misal /produk jadi /katalog), kamu cukup mengubah satu baris di routes/web.php — semua link di aplikasi otomatis mengikuti, tanpa perlu mencari-cari satu per satu.
Route group
// kelompokkan route dengan prefix dan middleware yang sama (Materi 18)
Route::prefix('admin')->group(function () {
Route::get('/dashboard', function () { return 'Dashboard admin'; });
Route::get('/produk', [ProdukController::class, 'index']);
});
// kedua route di atas otomatis menjadi /admin/dashboard dan /admin/produkResource route: satu baris untuk seluruh CRUD
// satu baris ini menghasilkan 7 route CRUD standar sekaligus
Route::resource('produk', ProdukController::class);| Method | URL | Fungsi |
|---|---|---|
| GET | /produk | index — daftar semua |
| GET | /produk/create | create — form tambah |
| POST | /produk | store — simpan baru |
| GET | /produk/{id} | show — detail satu |
| GET | /produk/{id}/edit | edit — form ubah |
| PUT | /produk/{id} | update — simpan perubahan |
| DELETE | /produk/{id} | destroy — hapus |
Catatan — Bandingkan satu baris Route::resource(...) ini dengan tabel $routes manual yang kamu tulis satu per satu di studi kasus PHP (Materi 4 & 11) — inilah gambaran nyata seberapa besar waktu yang dihemat framework, untuk pola yang hampir selalu sama di setiap fitur CRUD.
Latihan — Buat route untuk resource "kategori": index, show, dan create secara manual satu per satu (bukan pakai Route::resource), masing-masing mengembalikan teks berbeda. Beri nama tiap route-nya. Lalu ganti dengan satu baris Route::resource dan bandingkan — pastikan semua URL yang sama masih bisa diakses.
Materi 4 · Modul 0
Blade: template engine Laravel
Cara menampilkan HTML dinamis — versi jauh lebih rapi dari mencampur PHP dan HTML manual.
View pertama
<!DOCTYPE html>
<html>
<head><title>Halo</title></head>
<body>
<h1>Halo, {{ $nama }}!</h1>
</body>
</html>Route::get('/halo', function () {
return view('halo', ['nama' => 'Nus']);
// "halo" merujuk resources/views/halo.blade.php
});Catatan — {{ $nama }} menampilkan nilai variabel, dan Blade otomatis membungkusnya dengan htmlspecialchars() — perlindungan XSS (Materi 15 handbook PHP) yang di kode manual harus kamu tulis sendiri di setiap tempat, sekarang jadi perilaku bawaan. Kalau benar-benar perlu menampilkan HTML mentah tanpa di-escape (jarang, dan berisiko), pakai {!! $html !!} — tiga karakter berbeda untuk menandai "ini sengaja tidak aman".
Kondisi dan perulangan di Blade
@if ($produk->stok > 0)
<p>Tersedia</p>
@elseif ($produk->stok === 0)
<p>Habis</p>
@else
<p>Status tidak diketahui</p>
@endif
<ul>
@foreach ($daftarProduk as $produk)
<li>{{ $produk->nama }} — Rp {{ number_format($produk->harga, 0, ',', '.') }}</li>
@endforeach
</ul>
@forelse ($daftarProduk as $produk)
<li>{{ $produk->nama }}</li>
@empty
<li>Belum ada produk</li>
@endforelseCatatan — Bandingkan dengan gaya <?php if (...): ?> ... <?php endif; ?> di handbook PHP Materi 12 — Blade menyederhanakan sintaks itu jadi @if/@endif, sedikit lebih ringkas dibaca. @forelse adalah tambahan praktis: dia otomatis menangani kasus array kosong (lewat @empty) tanpa perlu if (empty(...)) terpisah.
Layout: menghindari duplikasi HTML
Ingat pola header.php/footer.php di handbook PHP Materi 8? Blade punya cara yang jauh lebih rapi lewat layout dan @yield.
<!DOCTYPE html>
<html>
<head><title>@yield('title', 'Toko Laravel')</title></head>
<body>
<nav>Menu di sini</nav>
<!-- @yield menandai "lubang" yang diisi tiap halaman turunan -->
@yield('content')
</body>
</html>@extends('layouts.app')
@section('title', 'Daftar Produk')
@section('content')
<h1>Daftar Produk</h1>
@foreach ($produk as $p)
<p>{{ $p->nama }}</p>
@endforeach
@endsectionCatatan — @extends berarti "gunakan layout ini sebagai bingkai". @section('content')...@endsection mengisi "lubang" bernama content yang ditandai @yield('content') di layout. Sekarang navbar dan struktur HTML dasar cukup ditulis sekali di layout — setiap halaman baru cukup mengisi bagian isinya.
Blade component: potongan UI yang bisa dipakai ulang
<div class="kartu">
<h3>{{ $nama }}</h3>
<p>Rp {{ number_format($harga, 0, ',', '.') }}</p>
</div>@foreach ($produk as $p)
<x-kartu-produk :nama="$p->nama" :harga="$p->harga" />
@endforeachDibahas lebih dalam di Materi 14. Untuk sekarang, cukup tahu: Blade component adalah cara Laravel membuat "potongan HTML" yang bisa dipakai berulang dengan data berbeda — mirip konsep <x-kartu-produk /> di React yang mungkin sudah kamu kenal, tapi murni server-side.
Latihan — Buat layout layouts/app.blade.php dengan navbar sederhana. Buat dua halaman (beranda.blade.php dan tentang.blade.php) yang memakai @extends dari layout itu, masing-masing dengan judul dan isi berbeda. Buat route untuk keduanya dan pastikan navbar konsisten muncul di kedua halaman.
Materi 5 · Modul 0
Artisan CLI
Command-line tool yang akan kamu jalankan berkali-kali sehari — pembuat file otomatis dan banyak lagi.
Membuat file secara otomatis
php artisan make:controller ProdukController
php artisan make:model Produk
php artisan make:model Produk -m # -m = sekalian buat migration-nya
php artisan make:migration create_produk_table
php artisan make:request StoreProdukRequest
php artisan make:seeder ProdukSeeder
php artisan make:factory ProdukFactory
php artisan make:middleware CekAdmin
php artisan make:policy ProdukPolicy
php artisan make:test ProdukTestCatatan — Bandingkan dengan handbook PHP Materi 21, di mana kamu memasang vlucas/phpdotenv lewat Composer dan menulis struktur file manual. artisan make:* adalah generator yang membuat file dengan struktur dasar sudah terisi (namespace, class kosong, import yang relevan) — kamu tinggal mengisi logikanya, bukan mengetik boilerplate dari nol tiap kali.
Perintah yang sering dipakai
| Perintah | Fungsi |
|---|---|
php artisan serve | Jalankan server development. |
php artisan migrate | Jalankan migration — buat/ubah tabel database (Materi 6). |
php artisan migrate:fresh --seed | Hapus semua tabel, buat ulang, isi data awal — sangat berguna saat development. |
php artisan tinker | REPL interaktif — coba kode Laravel/Eloquent langsung di terminal. |
php artisan route:list | Lihat semua route yang terdaftar di aplikasi. |
php artisan db:seed | Jalankan seeder — isi data awal/dummy. |
php artisan config:clear | Bersihkan cache konfigurasi (berguna kalau perubahan .env tidak "kena"). |
Mencoba tinker
php artisan tinker
>>> 2 + 2
= 4
>>> echo "Halo dari tinker";
Halo dari tinker
>>> App\Models\Produk::count(); // setelah model & migration ada (Materi 6-7)
= 0Catatan — tinker adalah cara tercepat mencoba potongan kode Laravel — query database, memanggil method model, mengecek konfigurasi — tanpa harus membuat route atau file baru dulu. Akan sangat sering kamu pakai mulai Modul 1 saat belajar Eloquent.
Membuat command Artisan sendiri
php artisan make:command KirimLaporanHarian<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class KirimLaporanHarian extends Command {
protected $signature = 'laporan:kirim'; // nama yang dipanggil di terminal
protected $description = 'Kirim laporan penjualan harian';
public function handle(): void {
$this->info('Mengirim laporan...');
// ... logika di sini ...
$this->info('Selesai!');
}
}php artisan laporan:kirimIni dibahas lebih dalam di Modul 5 — perintah custom sangat berguna untuk tugas terjadwal (kirim email harian, bersihkan data lama) yang dijalankan lewat cron job, mirip contoh mysqldump terjadwal di handbook MySQL Materi 25.
Latihan — Jalankan php artisan route:list pada project latihanmu dan amati semua route yang sudah terdaftar dari materi sebelumnya. Coba php artisan tinker dan jalankan beberapa ekspresi PHP sederhana di dalamnya. Buat satu command custom bernama salam:pagi yang menampilkan "Selamat pagi!" saat dijalankan.
Materi 6 · Modul 1
Migration: struktur database sebagai kode
Menulis skema tabel dengan PHP, bukan SQL manual — supaya perubahan struktur terlacak dan mudah dibagikan ke tim.
Kenapa migration, bukan SQL langsung
Di handbook MySQL, kamu menulis database/skema.sql lalu menjalankannya manual (Materi 3 studi kasus). Masalahnya: kalau strukturnya berubah nanti, atau ada anggota tim lain yang perlu setup database yang sama persis, mereka harus tahu urutan perubahan apa saja yang pernah terjadi. Migration menyimpan tiap perubahan struktur sebagai file terpisah dan berurutan — riwayat lengkap struktur database, bisa dijalankan ulang kapan saja di mesin manapun.
Membuat dan menulis migration
php artisan make:migration create_produk_table<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('produk', function (Blueprint $table) {
$table->id(); // kolom id, auto increment, primary key
$table->string('nama');
$table->unsignedInteger('harga');
$table->unsignedInteger('stok')->default(0);
$table->string('gambar')->nullable();
$table->timestamps(); // created_at & updated_at otomatis
$table->softDeletes(); // kolom deleted_at, untuk soft delete
});
}
public function down(): void {
Schema::dropIfExists('produk'); // cara "membatalkan" migration ini
}
};Catatan — up() berisi perubahan yang dijalankan, down() berisi cara membatalkannya — mirip prinsip COMMIT/ROLLBACK di handbook MySQL Materi 22, tapi untuk perubahan struktur, bukan data. timestamps() dan softDeletes() adalah "singkatan" untuk pola yang sangat umum kamu tulis manual di skema studi kasus (dibuat_pada, dihapus_pada).
Tipe kolom yang umum
| Blueprint | Setara SQL (handbook MySQL) |
|---|---|
$table->id() | id INT AUTO_INCREMENT PRIMARY KEY |
$table->string('nama') | VARCHAR(255) |
$table->string('nama', 100) | VARCHAR(100) |
$table->text('deskripsi') | TEXT |
$table->integer('stok') | INT |
$table->decimal('harga', 10, 2) | DECIMAL(10,2) |
$table->boolean('aktif') | TINYINT(1) |
$table->enum('status', [...]) | ENUM(...) |
$table->foreignId('kategori_id') | INT + siap untuk foreign key |
Foreign key di migration
Schema::create('produk', function (Blueprint $table) {
$table->id();
$table->foreignId('kategori_id')
->constrained() // otomatis REFERENCES kategori(id)
->onDelete('restrict'); // sama seperti ON DELETE RESTRICT, Materi 17 handbook MySQL
$table->string('nama');
$table->timestamps();
});Catatan — foreignId('kategori_id')->constrained() secara otomatis menyimpulkan bahwa dia merujuk ke tabel kategori (bentuk jamak dari nama tanpa _id) dan kolom id-nya — persis pola FOREIGN KEY (kategori_id) REFERENCES kategori(id) yang kamu tulis manual di handbook MySQL Materi 17.
Menjalankan migration
php artisan migrate # jalankan migration yang belum pernah dijalankan
php artisan migrate:status # lihat mana yang sudah/belum jalan
php artisan migrate:rollback # batalkan batch migration TERAKHIR (panggil down())
php artisan migrate:fresh # hapus SEMUA tabel, jalankan ulang dari awal — HANYA untuk developmentHati-hati — migrate:fresh menghapus seluruh data di database — jangan pernah menjalankannya di database produksi yang berisi data sungguhan. Laravel mencatat migration mana saja yang sudah dijalankan di tabel khusus bernama migrations, supaya php artisan migrate tahu mana yang perlu dijalankan dan mana yang dilewati.
Latihan — Buat migration untuk tabel kategori (id, nama, timestamps) dan produk (id, kategori_id dengan foreign key, nama, harga, stok, timestamps, softDeletes). Jalankan php artisan migrate dan verifikasi struktur tabelnya sesuai lewat DESCRIBE produk; di MySQL langsung.
Materi 7 · Modul 1
Eloquent: model dasar
ORM Laravel — objek yang "berbicara" langsung ke database, menggantikan query manual lewat PDO.
Model pertama
php artisan make:model Produk<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Produk extends Model {
// itu saja — Laravel MENEBAK nama tabel "produk" dari nama class "Produk"
}Catatan — Eloquent mengikuti konvensi: class Produk (tunggal) otomatis dipetakan ke tabel produk (dugaan bentuk jamak dalam bahasa Inggris — untuk nama Indonesia seperti ini kebetulan sama). Kalau nama tabelmu tidak mengikuti pola itu, kamu bisa menentukannya manual dengan protected $table = 'nama_tabel';.
Membaca data (Read)
use App\Models\Produk;
Produk::all(); // SELECT * FROM produk — semua baris
Produk::find(1); // SELECT * FROM produk WHERE id = 1 — satu baris, atau null
Produk::findOrFail(1); // sama, tapi lempar 404 otomatis kalau tidak ketemu
Produk::where('stok', '>', 0)->get(); // SELECT * FROM produk WHERE stok > 0
Produk::where('harga', '>=', 50000)->first(); // baris PERTAMA yang cocok
Produk::count(); // SELECT COUNT(*) FROM produkCatatan — Bandingkan Produk::where('stok', '>', 0)->get() dengan $pdo->prepare("SELECT * FROM produk WHERE stok > ?"); $stmt->execute([0]); di studi kasus PHP+MySQL Materi 8 — hasilnya SAMA PERSIS (dan Eloquent tetap memakai prepared statement di baliknya, aman dari SQL injection), cuma jauh lebih ringkas ditulis.
Menyimpan data (Create)
// cara 1: buat object, isi, simpan
$produk = new Produk();
$produk->nama = 'Kaos Polos';
$produk->harga = 85000;
$produk->save();
// cara 2: create() langsung dengan array — lebih ringkas
Produk::create([
'nama' => 'Kaos Polos',
'harga' => 85000,
'stok' => 24,
]);Hati-hati — create() tidak akan langsung bekerja sebelum kamu mendaftarkan properti mana saja yang boleh diisi massal (mass assignment) — perlindungan supaya field sensitif (seperti peran atau saldo) tidak bisa diubah tak sengaja lewat input form. Dibahas mendalam di Materi 21.
class Produk extends Model {
protected $fillable = ['nama', 'harga', 'stok', 'kategori_id', 'gambar'];
}$fillable ini persis konsep DisallowUnknownFields() di handbook Go, atau validasi manual field yang dibahas di handbook PHP Materi 15 — daftar putih kolom yang aman diisi dari input pengguna.
Mengubah data (Update)
$produk = Produk::find(1);
$produk->harga = 95000;
$produk->save();
// atau langsung dengan update()
Produk::find(1)->update(['harga' => 95000]);Menghapus data (Delete)
$produk = Produk::find(1);
$produk->delete();
// kalau model pakai SoftDeletes (Materi 6), ini otomatis SOFT delete, bukan DELETE sungguhanuse Illuminate\Database\Eloquent\SoftDeletes;
class Produk extends Model {
use SoftDeletes; // syarat: tabel harus punya kolom deleted_at (softDeletes() di migration)
}Catatan — Dengan trait SoftDeletes ditambahkan, Produk::all() secara otomatis mengecualikan baris yang sudah "dihapus" — kamu tidak perlu menulis WHERE dihapus_pada IS NULL manual di setiap query seperti di repository studi kasus PHP+MySQL Materi 8. Untuk melihat yang sudah terhapus: Produk::withTrashed()->get(); untuk memulihkan: $produk->restore().
Latihan — Buat model Produk dengan $fillable yang sesuai dan trait SoftDeletes. Lewat php artisan tinker: buat 3 produk dengan create(), ambil semuanya dengan all(), ubah salah satu dengan update(), hapus satu dengan delete(), lalu pastikan yang terhapus tidak muncul di Produk::all() tapi tetap ada di Produk::withTrashed()->get().
Materi 8 · Modul 1
Query builder dan Eloquent lanjutan
Menyusun query yang lebih kompleks — pencarian, pengurutan, pagination, dan agregasi.
Merangkai kondisi query
Produk::where('stok', '>', 0)
->where('harga', '<', 100000)
->orderBy('nama', 'asc')
->get();
// LIKE untuk pencarian — persis Materi 7 handbook MySQL
Produk::where('nama', 'like', "%{$kataKunci}%")->get();
// whereIn — beberapa nilai sekaligus
Produk::whereIn('id', [1, 2, 3])->get();
// orWhere — kondisi OR
Produk::where('stok', 0)->orWhere('harga', '>', 500000)->get();Catatan — Setiap method di atas selalu aman dari SQL injection — nilai yang kamu berikan ($kataKunci, angka, dll) otomatis dikirim sebagai parameter terpisah lewat prepared statement di baliknya, persis prinsip Materi 15 handbook PHP, walau kamu tidak menulis ? dan execute([...]) manual sama sekali.
Pagination: jauh lebih ringkas dari manual
// satu baris ini menggantikan LIMIT/OFFSET manual + hitung total halaman
// (bandingkan dengan Materi 9 studi kasus PHP+MySQL yang menulis semuanya manual)
$produk = Produk::paginate(10);@foreach ($produk as $p)
<p>{{ $p->nama }}</p>
@endforeach
{{ $produk->links() }} // menghasilkan tombol navigasi halaman lengkap, otomatisCatatan — paginate(10) otomatis membaca parameter ?page= dari URL, menghitung OFFSET yang tepat, DAN menghitung total halaman — semua yang kamu tulis manual di hitungTotal() + LIMIT ... OFFSET ... pada studi kasus Materi 9. {{ $produk->links() }} bahkan menghasilkan tombol navigasi halaman lengkap dengan styling dasar.
Agregasi
Produk::sum('stok'); // SUM(stok)
Produk::avg('harga'); // AVG(harga)
Produk::max('harga'); // MAX(harga)
Produk::where('stok', 0)->count(); // COUNT(*) WHERE stok = 0
// group by
Produk::selectRaw('kategori_id, COUNT(*) as jumlah')
->groupBy('kategori_id')
->get();Ini persis fungsi agregat di handbook MySQL Materi 11–12, sekarang dipanggil sebagai method PHP alih-alih menulis SQL langsung.
Raw query: saat butuh SQL langsung
use Illuminate\Support\Facades\DB;
// untuk query kompleks yang sulit diekspresikan lewat Eloquent
$hasil = DB::select('SELECT * FROM produk WHERE stok > ?', [0]);
// SELALU pakai placeholder ?, JANGAN sambung string manual — sama seperti Materi 15 handbook PHPHati-hati — Eloquent mencakup hampir semua kebutuhan sehari-hari, tapi untuk query yang sangat kompleks (window function, subquery rumit dari handbook MySQL Materi 15–16), kadang raw query lebih jelas. Aturan yang tidak berubah: selalu pakai placeholder ? dengan array parameter terpisah — jangan pernah menyambung input pengguna langsung ke string SQL, bahkan di Laravel.
Latihan — Dengan data produk dari latihan sebelumnya, tulis query Eloquent untuk: produk dengan stok di bawah 10 diurutkan dari paling sedikit, produk dengan nama mengandung kata tertentu, dan total nilai inventori (SUM harga × stok — coba dengan selectRaw). Tampilkan hasil paginate(5) di sebuah view Blade sederhana.
Materi 9 · Modul 1
Relasi Eloquent
Mengekspresikan hubungan antar tabel (Materi 17–18 handbook MySQL) sebagai method PHP yang natural dipakai.
One-to-many: belongsTo dan hasMany
class Kategori extends Model {
public function produk() {
return $this->hasMany(Produk::class); // satu kategori PUNYA BANYAK produk
}
}class Produk extends Model {
public function kategori() {
return $this->belongsTo(Kategori::class); // satu produk MILIK SATU kategori
}
}$produk = Produk::find(1);
$produk->kategori->nama; // akses seperti PROPERTI, bukan method — Eloquent yang query otomatis
$kategori = Kategori::find(1);
foreach ($kategori->produk as $p) {
echo $p->nama;
}Catatan — Bandingkan dengan JOIN manual di handbook MySQL Materi 13: $produk->kategori di baliknya menjalankan query SELECT * FROM kategori WHERE id = ? otomatis begitu kamu mengaksesnya. belongsTo dipasang di sisi yang punya foreign key (Materi 18 handbook MySQL — "FK selalu di sisi many"), hasMany di sisi induknya.
Many-to-many: belongsToMany
class Pesanan extends Model {
public function produk() {
return $this->belongsToMany(Produk::class, 'item_pesanan')
->withPivot('qty', 'harga_saat_beli') // kolom TAMBAHAN di tabel pivot
->withTimestamps();
}
}$pesanan = Pesanan::find(1);
foreach ($pesanan->produk as $p) {
echo $p->nama;
echo $p->pivot->qty; // akses kolom dari tabel PIVOT lewat ->pivot
echo $p->pivot->harga_saat_beli;
}
// menambah relasi (mengisi tabel pivot)
$pesanan->produk()->attach($produkId, ['qty' => 2, 'harga_saat_beli' => 85000]);Catatan — Ini persis konsep tabel pivot item_pesanan yang dirancang manual di handbook MySQL Materi 18 dan dipakai lewat query manual di studi kasus PHP+MySQL Materi 15. ->pivot adalah cara Eloquent mengakses kolom tambahan (seperti qty) yang tersimpan di tabel penghubung itu sendiri, bukan di tabel produk.
Eager loading: mencegah masalah N+1
$produk = Produk::all(); // 1 query
foreach ($produk as $p) {
echo $p->kategori->nama; // 1 query TERPISAH untuk TIAP produk — persis Materi 27 handbook MySQL!
}
// 100 produk = 101 query total$produk = Produk::with('kategori')->get(); // HANYA 2 query total, berapa pun jumlah produk
foreach ($produk as $p) {
echo $p->kategori->nama; // TIDAK ada query tambahan — sudah dimuat sekaligus di awal
}Hati-hati — Masalah N+1 yang dibahas panjang lebar di handbook MySQL Materi 27 bisa diam-diam terjadi di Eloquent kalau kamu tidak sadar sedang mengakses relasi di dalam loop. Aturan praktis: begitu kamu tahu akan mengakses relasi sebuah model dalam loop, tambahkan ->with('namaRelasi') saat mengambil datanya. Laravel Debugbar (paket populer) bisa membantu melihat jumlah query aktual yang terjadi saat development.
Latihan — Definisikan relasi hasMany/belongsTo antara Kategori dan Produk. Di tinker, ambil semua produk dengan with('kategori') dan tanpa itu, bandingkan lewat DB::enableQueryLog() dan DB::getQueryLog() berapa banyak query yang terjadi di masing-masing cara.
Materi 10 · Modul 1
Seeder dan factory
Mengisi database dengan data awal atau data dummy dalam jumlah besar — otomatis, bukan INSERT manual satu-satu.
Seeder: data awal yang pasti
php artisan make:seeder KategoriSeeder<?php
namespace Database\Seeders;
use App\Models\Kategori;
use Illuminate\Database\Seeder;
class KategoriSeeder extends Seeder {
public function run(): void {
$kategori = ['Pakaian', 'Aksesoris', 'Sepatu'];
foreach ($kategori as $nama) {
Kategori::create(['nama' => $nama]);
}
}
}public function run(): void {
$this->call([
KategoriSeeder::class, // urutan penting — kategori harus ada SEBELUM produk (foreign key)
ProdukSeeder::class,
]);
}php artisan db:seed
# atau sekaligus dengan migration: reset total + isi ulang
php artisan migrate:fresh --seedFactory: banyak data dummy dengan pola acak
php artisan make:factory ProdukFactory --model=Produk<?php
namespace Database\Factories;
use App\Models\Kategori;
use Illuminate\Database\Eloquent\Factories\Factory;
class ProdukFactory extends Factory {
public function definition(): array {
return [
'kategori_id' => Kategori::factory(), // buat kategori BARU otomatis kalau perlu
'nama' => $this->faker->words(3, true),
'harga' => $this->faker->numberBetween(10000, 500000),
'stok' => $this->faker->numberBetween(0, 50),
];
}
}Produk::factory()->count(50)->create(); // 50 produk PALSU dengan data acak, langsung tersimpan
// override sebagian data, sisanya tetap acak
Produk::factory()->create(['nama' => 'Produk Spesial']);Catatan — faker adalah library bawaan Laravel yang menghasilkan data acak realistis (nama, angka, teks) — sangat berguna untuk development dan testing, saat kamu butuh data dalam jumlah besar untuk mencoba pagination, pencarian, atau performa (persis kebutuhan yang disebutkan di handbook MySQL Materi 9, "isi minimal 15 produk dummy"). Kategori bekerja bersama Produk lewat with() di Materi 9 — Kategori::factory() di dalam ProdukFactory otomatis membuat kategori terkait kalau belum ada.
Perbedaan seeder dan factory
| Alat | Untuk |
|---|---|
| Seeder | Data yang pasti/tetap — kategori toko, akun admin pertama, konfigurasi awal. |
| Factory | Data dummy dalam jumlah besar — untuk development dan testing, TIDAK dipakai di produksi. |
Latihan — Buat KategoriSeeder dengan 3 kategori tetap. Buat ProdukFactory yang menghasilkan produk acak terkait kategori yang ada. Jalankan migrate:fresh --seed yang memanggil keduanya (kategori dulu, lalu 30 produk lewat factory di ProdukSeeder). Verifikasi datanya di tinker.
Materi 11 · Modul 1
Eloquent lanjutan: accessor, mutator, scope
Memindahkan logika yang berulang ke dalam model itu sendiri — kode yang lebih bersih di seluruh aplikasi.
Accessor: mengubah cara data ditampilkan
use Illuminate\Database\Eloquent\Casts\Attribute;
class Produk extends Model {
// accessor: menambah properti VIRTUAL "harga_format" tanpa kolom baru di database
protected function hargaFormat(): Attribute {
return Attribute::make(
get: fn (mixed $value, array $attributes) =>
'Rp ' . number_format($attributes['harga'], 0, ',', '.'),
);
}
}$produk = Produk::find(1);
echo $produk->harga_format; // "Rp 85.000" — diakses seperti properti biasaCatatan — Ini menggantikan method hargaFormat() yang kamu tulis manual di class Produk pada studi kasus PHP+MySQL Materi 8 — bedanya, dengan accessor, dia terasa seperti properti biasa ($produk->harga_format, bukan $produk->hargaFormat()) dan otomatis "hidup" di setiap tempat model itu dipakai, termasuk saat diubah jadi JSON untuk API (Materi 22).
Mutator: mengubah data sebelum disimpan
protected function nama(): Attribute {
return Attribute::make(
set: fn (string $value) => trim($value), // otomatis trim() SETIAP kali disimpan
);
}Mutator berjalan otomatis setiap kali properti itu diisi ($produk->nama = " Kaos " atau lewat create([...])) — memastikan aturan (seperti trim spasi) berlaku konsisten di seluruh aplikasi, tidak perlu diingat-ingat di setiap tempat yang menyimpan data.
Local scope: query yang bisa dipakai ulang
public function scopeTersedia($query) {
return $query->where('stok', '>', 0);
}
public function scopeMurah($query, int $maksimal) {
return $query->where('harga', '<=', $maksimal);
}Produk::tersedia()->get();
Produk::tersedia()->murah(50000)->get(); // bisa dirangkai, seperti Materi 9 handbook JS!
Produk::tersedia()->murah(50000)->paginate(10);Catatan — Scope adalah cara memberi nama pada kondisi query yang sering dipakai berulang — sama seperti method validasiProduk() yang dipakai ulang di studi kasus PHP+MySQL Materi 11, tapi untuk query. Kalau kamu menemukan diri menulis ->where('stok', '>', 0) di banyak tempat berbeda di aplikasi, itu sinyal kuat untuk dijadikan scope.
Casting: tipe data otomatis
protected function casts(): array {
return [
'aktif' => 'boolean', // 0/1 dari database jadi true/false di PHP
'metadata' => 'array', // kolom JSON otomatis jadi array PHP dan sebaliknya
'harga' => 'integer',
];
}Tanpa cast, kolom metadata bertipe JSON (Materi 4 handbook MySQL) akan diterima sebagai string mentah — kamu harus json_decode() manual tiap kali. Dengan 'array' cast, Eloquent melakukannya otomatis di kedua arah.
Latihan — Tambahkan accessor harga_format dan scope tersedia() serta murah(int $maksimal) ke model Produk. Uji di tinker: rangkai Produk::tersedia()->murah(100000)->get() dan cetak harga_format tiap hasilnya.
Materi 12 · Modul 2
Controller dan resource route
Memindahkan logika dari route ke class terstruktur — persis pola ProdukController di studi kasus PHP+MySQL, kini dibantu banyak konvensi Laravel.
Membuat resource controller
php artisan make:controller ProdukController --resource --model=Produk<?php
namespace App\Http\Controllers;
use App\Models\Produk;
class ProdukController extends Controller {
public function index() {
$produk = Produk::with('kategori')->paginate(10);
return view('produk.index', compact('produk'));
}
public function create() {
$kategori = Kategori::all();
return view('produk.create', compact('kategori'));
}
public function store(Request $request) {
Produk::create($request->all());
return redirect()->route('produk.index')->with('sukses', 'Produk ditambahkan');
}
public function show(Produk $produk) {
return view('produk.show', compact('produk'));
}
public function edit(Produk $produk) {
$kategori = Kategori::all();
return view('produk.edit', compact('produk', 'kategori'));
}
public function update(Request $request, Produk $produk) {
$produk->update($request->all());
return redirect()->route('produk.index')->with('sukses', 'Produk diubah');
}
public function destroy(Produk $produk) {
$produk->delete();
return redirect()->route('produk.index')->with('sukses', 'Produk dihapus');
}
}Route::resource('produk', ProdukController::class); // dari Materi 3Catatan — Tujuh method (index, create, store, show, edit, update, destroy) ini persis nama-nama yang otomatis dihubungkan ke tujuh route dari Route::resource (Materi 3). Bandingkan strukturnya dengan ProdukController di studi kasus PHP+MySQL Materi 8–12 — nama method dan alurnya sengaja mirip, karena keduanya mengikuti pola CRUD yang sama; Laravel hanya menghubungkannya secara otomatis.
Route model binding: parameter otomatis jadi object
Perhatikan show(Produk $produk) di atas — parameternya langsung berupa object Produk, bukan $id mentah. Ini disebut route model binding.
public function show(string $id) {
$produk = Produk::findOrFail($id); // harus query manual, dan tangani 404 sendiri
return view('produk.show', compact('produk'));
}public function show(Produk $produk) {
// $produk SUDAH berupa object hasil query, dan otomatis 404 kalau id tidak ditemukan
return view('produk.show', compact('produk'));
}Catatan — Laravel melihat tipe parameter (Produk $produk), mengambil id dari URL (/produk/{produk}), lalu otomatis menjalankan Produk::findOrFail($id) di baliknya — termasuk melempar 404 otomatis kalau tidak ketemu, tanpa kamu menulis pengecekan manual seperti di controller studi kasus PHP+MySQL Materi 11.
Controller non-resource
php artisan make:controller DashboardControllerUntuk halaman yang tidak mengikuti pola CRUD (dashboard, halaman about, laporan), buat controller biasa tanpa --resource, dengan method dan route sesuai kebutuhan — persis DashboardController::index() di studi kasus PHP+MySQL Materi 16.
Latihan — Buat KategoriController sebagai resource controller lengkap (index, create, store, edit, update, destroy). Daftarkan sebagai Route::resource. Uji semua tujuh endpoint-nya lewat browser atau Postman (untuk method PUT/DELETE bisa dites dari form dulu, atau nanti setelah Materi 14).
Materi 13 · Modul 2
Form Request dan validasi
Sistem validasi bawaan Laravel — menggantikan class Validator manual yang dibangun di studi kasus PHP+MySQL Materi 17.
Validasi langsung di controller
public function store(Request $request) {
$data = $request->validate([
'nama' => 'required|string|max:150',
'harga' => 'required|integer|min:1',
'stok' => 'required|integer|min:0',
'kategori_id' => 'required|exists:kategori,id',
]);
// kalau validasi GAGAL, Laravel otomatis redirect balik dengan error — tanpa kamu menulis apa pun
Produk::create($data);
return redirect()->route('produk.index');
}Catatan — $request->validate([...]) ini menggantikan seluruh isi class Validator yang ditulis manual di studi kasus PHP+MySQL Materi 17 — required (setara ->wajib()), max:150 (setara ->minPanjang() versi kebalikan), dan puluhan aturan siap pakai lainnya (email, unique, confirmed, dll) tanpa kamu menulis satu pun logika pengecekan sendiri.
Menampilkan error di Blade
<form method="POST" action="{{ route('produk.store') }}">
@csrf {{-- token CSRF otomatis, setara Csrf::field() manual di studi kasus Materi 18 --}}
<input type="text" name="nama" value="{{ old('nama') }}">
@error('nama')
<p>{{ $message }}</p>
@enderror
<input type="number" name="harga" value="{{ old('harga') }}">
@error('harga')
<p>{{ $message }}</p>
@enderror
<button>Simpan</button>
</form>Catatan — old('nama') mengisi ulang form dengan input sebelumnya kalau validasi gagal — otomatis, tanpa kamu menulis $_POST['nama'] ?? '' manual seperti di handbook PHP Materi 12. @error('nama')...@enderror hanya menampilkan isinya kalau field itu memang gagal validasi. @csrf satu directive ini menggantikan seluruh class Csrf manual yang ditulis di studi kasus PHP+MySQL Materi 18.
Form Request: memindahkan validasi ke class sendiri
Untuk form yang validasinya kompleks, pindahkan ke class terpisah — controller jadi lebih bersih, dan aturan validasinya reusable.
php artisan make:request StoreProdukRequest<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProdukRequest extends FormRequest {
public function authorize(): bool {
return true; // atau cek izin di sini (Materi 19)
}
public function rules(): array {
return [
'nama' => 'required|string|max:150',
'harga' => 'required|integer|min:1',
'stok' => 'required|integer|min:0',
'kategori_id' => 'required|exists:kategori,id',
];
}
public function messages(): array {
return [
'nama.required' => 'Nama produk wajib diisi',
'harga.min' => 'Harga harus lebih dari 0',
];
}
}use App\Http\Requests\StoreProdukRequest;
public function store(StoreProdukRequest $request) {
// begitu masuk sini, data SUDAH divalidasi — kalau gagal, Laravel sudah redirect balik duluan
Produk::create($request->validated());
return redirect()->route('produk.index');
}Catatan — Cukup mengganti tipe parameter dari Request jadi StoreProdukRequest, Laravel otomatis menjalankan validasinya sebelum method controller dijalankan sama sekali. $request->validated() mengembalikan hanya data yang sudah lolos aturan — mirip fungsi validasiProduk() terpisah di studi kasus PHP+MySQL Materi 10, tapi dengan lebih banyak aturan siap pakai dan pesan error yang bisa dikustomisasi per field.
Latihan — Buat StoreProdukRequest dan UpdateProdukRequest dengan aturan yang sesuai. Pasang di method store dan update controller. Uji: submit form dengan data kosong (pastikan error tampil dengan pesan kustom), dan dengan data valid (pastikan tersimpan).
Materi 14 · Modul 2
Blade layout dan component lanjutan
Menyusun tampilan CRUD lengkap dengan struktur yang tidak berulang-ulang.
@include: menyisipkan potongan view
{{-- form ini DIPAKAI ULANG untuk create DAN edit --}}
<input type="text" name="nama" value="{{ old('nama', $produk->nama ?? '') }}">
@error('nama') <p>{{ $message }}</p> @enderror
<select name="kategori_id">
@foreach ($kategori as $k)
<option value="{{ $k->id }}"
{{ (isset($produk) && $produk->kategori_id == $k->id) ? 'selected' : '' }}>
{{ $k->nama }}
</option>
@endforeach
</select>@extends('layouts.app')
@section('content')
<form method="POST" action="{{ route('produk.store') }}">
@csrf
@include('produk._form') {{-- sisipkan form yang sama --}}
<button>Simpan</button>
</form>
@endsection@extends('layouts.app')
@section('content')
<form method="POST" action="{{ route('produk.update', $produk) }}">
@csrf
@method('PUT') {{-- browser HTML cuma kenal GET/POST — @method "menyamar" jadi PUT --}}
@include('produk._form')
<button>Update</button>
</form>
@endsectionCatatan — @method('PUT') menyisipkan input tersembunyi yang memberi tahu Laravel "perlakukan POST ini sebagai PUT" — karena form HTML murni cuma mendukung GET dan POST. Ini pola yang lebih rapi dari trik _method manual yang mungkin pernah kamu temui di framework lain.
Blade component dengan class
php artisan make:component Alertclass Alert extends Component {
public function __construct(public string $type = 'info') {}
public function render() {
return view('components.alert');
}
}<div class="alert alert-{{ $type }}">
{{ $slot }} {{-- konten di ANTARA tag komponen ditaruh di sini --}}
</div><x-alert type="success">
Produk berhasil disimpan!
</x-alert>$slot adalah kekuatan komponen yang berbeda dari @include — dia bisa menerima konten HTML apa pun di antara tag pembuka-penutupnya, mirip cara children bekerja di React kalau kamu pernah mempelajarinya.
Latihan — Buat _form.blade.php untuk produk yang dipakai ulang di create dan edit. Buat komponen <x-alert> dan pakai untuk menampilkan pesan flash (dibahas lengkap Materi 15) dengan variasi warna berbeda (success, error).
Materi 15 · Modul 2
Session, flash message, dan redirect
Mengirim pesan singkat antar halaman setelah sebuah aksi — versi otomatis dari class Flash manual di studi kasus.
Flash message bawaan
public function store(StoreProdukRequest $request) {
Produk::create($request->validated());
return redirect()
->route('produk.index')
->with('sukses', 'Produk berhasil ditambahkan'); // flash message dalam SATU baris
}@if (session('sukses'))
<div class="alert-success">{{ session('sukses') }}</div>
@endifCatatan — ->with('sukses', '...') menyimpan pesan yang otomatis hilang setelah dibaca sekali — persis konsep Flash::set()/Flash::ambil() yang ditulis manual di studi kasus PHP+MySQL Materi 7, tapi dibangun dalam satu pemanggilan method saja, tanpa perlu class terpisah.
Session untuk data lain
use Illuminate\Support\Facades\Session;
// menyimpan
session(['keranjang' => $items]);
Session::put('keranjang', $items); // cara alternatif, sama fungsinya
// membaca
$keranjang = session('keranjang', []); // [] = nilai default kalau belum ada
// menghapus
session()->forget('keranjang');Ini bekerja persis seperti $_SESSION di handbook PHP Materi 13 — cuma dibungkus lewat helper session() yang lebih rapi. Laravel otomatis memanggil session_start() di baliknya, jadi kamu tidak perlu memikirkannya sama sekali.
redirect() dalam berbagai bentuk
redirect()->route('produk.index'); // ke named route
redirect()->route('produk.show', $produk); // dengan parameter
redirect('/produk'); // ke URL langsung
redirect()->back(); // ke halaman sebelumnya (berguna setelah validasi gagal)
redirect()->back()->withInput(); // kembali DENGAN input form tetap terisiValidasi manual dengan redirect balik
if ($stokTidakCukup) {
return back()->withErrors(['stok' => 'Stok tidak mencukupi']);
}Ini setara $errors["stok"] = "..." lalu menampilkan ulang form di studi kasus PHP+MySQL Materi 10 — withErrors() membuatnya bisa dibaca dengan @error('stok') di Blade (Materi 13), sama seperti error dari validasi otomatis.
Latihan — Tambahkan flash message "sukses" di method store, update, dan destroy pada ProdukController. Tampilkan pesan itu di layout dengan komponen <x-alert> dari Materi 14. Uji ketiga aksi dan pastikan pesan muncul sekali lalu hilang saat halaman di-refresh.
Materi 16 · Modul 2
Studi kasus mini: CRUD produk lengkap
Menyatukan Materi 12–15 jadi satu fitur utuh — dari route sampai tampil di browser.
Rangkaian lengkap
use App\Http\Controllers\ProdukController;
Route::resource('produk', ProdukController::class);<?php
namespace App\Http\Controllers;
use App\Models\Produk;
use App\Models\Kategori;
use App\Http\Requests\StoreProdukRequest;
use App\Http\Requests\UpdateProdukRequest;
class ProdukController extends Controller {
public function index(Request $request) {
$produk = Produk::with('kategori')
->when($request->cari, fn ($q) =>
$q->where('nama', 'like', "%{$request->cari}%"))
->paginate(10)
->withQueryString(); // pertahankan ?cari=... saat pindah halaman
return view('produk.index', compact('produk'));
}
public function create() {
return view('produk.create', ['kategori' => Kategori::all()]);
}
public function store(StoreProdukRequest $request) {
Produk::create($request->validated());
return redirect()->route('produk.index')->with('sukses', 'Produk ditambahkan');
}
public function edit(Produk $produk) {
return view('produk.edit', ['produk' => $produk, 'kategori' => Kategori::all()]);
}
public function update(UpdateProdukRequest $request, Produk $produk) {
$produk->update($request->validated());
return redirect()->route('produk.index')->with('sukses', 'Produk diubah');
}
public function destroy(Produk $produk) {
$produk->delete(); // soft delete otomatis (Materi 7)
return redirect()->route('produk.index')->with('sukses', 'Produk dihapus');
}
}Catatan — ->when($request->cari, fn ($q) => ...) adalah cara elegan menambahkan kondisi hanya kalau nilainya ada — setara if (!empty($_GET['cari'])) di studi kasus PHP+MySQL Materi 9, tapi dirangkai langsung dalam satu chain query. withQueryString() memastikan link pagination ({{ $produk->links() }}) tetap membawa parameter pencarian saat pindah halaman.
View index dengan pencarian dan pagination
@extends('layouts.app')
@section('content')
@if (session('sukses'))
<x-alert type="success">{{ session('sukses') }}</x-alert>
@endif
<form method="GET">
<input type="text" name="cari" value="{{ request('cari') }}" placeholder="Cari produk...">
<button>Cari</button>
</form>
<a href="{{ route('produk.create') }}">+ Tambah Produk</a>
<table>
@foreach ($produk as $p)
<tr>
<td>{{ $p->nama }}</td>
<td>{{ $p->kategori->nama }}</td>
<td>{{ $p->harga_format }}</td>
<td>
<a href="{{ route('produk.edit', $p) }}">Ubah</a>
<form method="POST" action="{{ route('produk.destroy', $p) }}" style="display:inline">
@csrf @method('DELETE')
<button onclick="return confirm('Yakin?')">Hapus</button>
</form>
</td>
</tr>
@endforeach
</table>
{{ $produk->links() }}
@endsectionBandingkan panjang dan kelengkapan view ini dengan versi manual di studi kasus PHP+MySQL Materi 9 — fungsinya identik (pencarian, pagination, tombol aksi, flash message), tapi yang perlu ditulis jauh lebih sedikit karena Eloquent, Blade directive, dan komponen menangani sebagian besar detail berulang.
Latihan — Satukan semua materi Modul 2: buat ProdukController resource lengkap dengan pencarian dan pagination, StoreProdukRequest/UpdateProdukRequest, view index/create/edit yang memakai _form.blade.php bersama, dan komponen <x-alert> untuk flash message. Uji seluruh alur CRUD dari browser.
Materi 17 · Modul 3
Autentikasi bawaan
Login, register, dan proteksi halaman siap pakai — menggantikan seluruh AuthController manual di studi kasus PHP+MySQL.
Memasang Laravel Breeze
composer require laravel/breeze --dev
php artisan breeze:install blade # pilih "blade" untuk view PHP biasa (bukan Vue/React)
npm install && npm run build
php artisan migrateCatatan — breeze:install membuat langsung: halaman login & register siap pakai, tabel users (lewat migration bawaan), hashing password dengan bcrypt (setara password_hash() di Materi 5 studi kasus), proteksi rate limiting untuk login (setara Materi 18 studi kasus), dan reset password. Ini menggantikan seluruh isi AuthController, PenggunaRepository, dan class Auth manual yang kamu tulis dari Materi 5–7 studi kasus PHP+MySQL.
Cara kerja di baliknya
use Illuminate\Support\Facades\Auth;
public function login(Request $request) {
$kredensial = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($kredensial)) { // cek email + password_verify di baliknya, otomatis
$request->session()->regenerate(); // setara session_regenerate_id() Materi 6 studi kasus
return redirect()->intended('/dashboard');
}
return back()->withErrors(['email' => 'Email atau password salah']);
}Ini persis alur di Materi 6 studi kasus PHP+MySQL, dan Auth::attempt() juga sengaja memberi pesan error generik yang sama untuk email tidak ditemukan maupun password salah — perlindungan terhadap user enumeration yang sama.
Mengakses user yang sedang login
Auth::user(); // object User yang sedang login, atau null
Auth::id(); // cuma id-nya
Auth::check(); // true/false — apakah ada yang login
// lewat helper singkat, sering dipakai di Blade
auth()->user();
auth()->user()->nama;@auth
<p>Halo, {{ auth()->user()->name }}</p>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button>Keluar</button>
</form>
@else
<a href="{{ route('login') }}">Masuk</a>
@endauth@auth/@endauth menggantikan pengecekan isset($_SESSION["user_id"]) manual di navbar Materi 6 studi kasus.
Menghubungkan produk dengan user pemiliknya
public function user() {
return $this->belongsTo(User::class);
}public function store(StoreProdukRequest $request) {
Produk::create($request->validated() + ['user_id' => auth()->id()]);
// atau: auth()->user()->produk()->create($request->validated());
}Latihan — Pasang Laravel Breeze di project latihanmu. Buat akun baru lewat halaman register, login, dan pastikan halaman /dashboard bawaan Breeze bisa diakses. Tambahkan sapaan nama pengguna di navbar dengan @auth.
Materi 18 · Modul 3
Middleware
Penyaring request sebelum sampai ke controller — versi terstruktur dari class Auth::wajibLogin() manual.
Middleware bawaan: auth
// satu route
Route::get('/dashboard', [DashboardController::class, 'index'])
->middleware('auth');
// grup route sekaligus — lebih umum dipakai
Route::middleware('auth')->group(function () {
Route::resource('produk', ProdukController::class);
Route::resource('kategori', KategoriController::class);
});Catatan — Middleware auth menggantikan pemanggilan Auth::wajibLogin() di baris pertama setiap method controller (studi kasus Materi 7) — sekarang cukup dideklarasikan sekali di route, otomatis berlaku untuk semua method di controller itu (atau semua route dalam grup). Kalau belum login, otomatis diarahkan ke halaman login.
Membuat middleware sendiri
php artisan make:middleware CekAdmin<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CekAdmin {
public function handle(Request $request, Closure $next) {
if (auth()->user()?->peran !== 'admin') {
abort(403, 'Akses ditolak — khusus admin');
}
return $next($request); // lanjutkan ke middleware berikutnya / controller
}
}$middleware->alias([
'admin' => \App\Http\Middleware\CekAdmin::class,
]);Route::middleware(['auth', 'admin'])->group(function () {
Route::delete('/produk/{produk}', [ProdukController::class, 'destroy']);
});Catatan — Ini persis konsep Auth::wajibAdmin() di studi kasus PHP+MySQL Materi 7, sekarang jadi class middleware yang bisa dipasang ke route mana pun tanpa mengubah isi controller sama sekali. $next($request) adalah "lanjutkan" — kalau tidak dipanggil, request berhenti di situ (seperti kasus abort(403) di atas).
Middleware berurutan
Middleware bisa dirangkai — ['auth', 'admin'] berarti request harus lolos auth dulu, baru admin. Kalau auth gagal, admin tidak pernah dijalankan sama sekali — mirip urutan Csrf::verifikasi() sebelum routing di public/index.php studi kasus Materi 18.
Latihan — Tambahkan kolom peran (enum admin/kasir) ke tabel users lewat migration baru. Buat middleware CekAdmin seperti di atas. Lindungi route destroy pada ProdukController dengan middleware admin, uji dengan akun berperan "kasir" (harus ditolak) dan "admin" (harus berhasil).
Materi 19 · Modul 3
Otorisasi: Gate dan Policy
Middleware menjawab "siapa boleh masuk"; Gate dan Policy menjawab pertanyaan lebih spesifik: "boleh melakukan aksi ini, pada data ini?"
Gate: aturan otorisasi sederhana
use Illuminate\Support\Facades\Gate;
public function boot(): void {
Gate::define('kelola-produk', function (User $user) {
return $user->peran === 'admin';
});
}// di controller
if (Gate::denies('kelola-produk')) {
abort(403);
}
// atau lebih ringkas
$this->authorize('kelola-produk'); // otomatis abort(403) kalau gagal@can('kelola-produk')
<a href="{{ route('produk.create') }}">+ Tambah Produk</a>
@endcanPolicy: otorisasi per model, lebih terstruktur
Untuk aturan yang berkaitan dengan model tertentu (siapa boleh mengubah produk tertentu, bukan cuma "produk secara umum"), Policy lebih rapi daripada Gate.
php artisan make:policy ProdukPolicy --model=Produk<?php
namespace App\Policies;
use App\Models\Produk;
use App\Models\User;
class ProdukPolicy {
public function update(User $user, Produk $produk): bool {
// admin boleh ubah semua produk, kasir cuma produk buatan sendiri
return $user->peran === 'admin' || $user->id === $produk->user_id;
}
public function delete(User $user, Produk $produk): bool {
return $user->peran === 'admin'; // hanya admin boleh hapus, siapa pun pembuatnya
}
}public function update(UpdateProdukRequest $request, Produk $produk) {
$this->authorize('update', $produk); // Laravel otomatis mencocokkan ke ProdukPolicy::update()
$produk->update($request->validated());
return redirect()->route('produk.index');
}@can('update', $produk)
<a href="{{ route('produk.edit', $produk) }}">Ubah</a>
@endcanCatatan — Laravel otomatis mencocokkan model (Produk) dengan Policy-nya (ProdukPolicy) berdasarkan konvensi nama. Kalau kamu perhatikan, ini persis pola pengecekan manual "$user->peran === 'admin' || $user->id === $produk->user_id" yang bisa saja tersebar di banyak controller — Policy mengumpulkan semua aturan otorisasi untuk satu model di satu tempat yang jelas.
Latihan — Buat ProdukPolicy dengan method update dan delete seperti contoh. Pasang $this->authorize() di controller, dan @can di view untuk menyembunyikan tombol yang tidak diizinkan. Uji dengan dua akun berbeda peran.
Materi 20 · Modul 3
File upload dan storage
Menyimpan gambar produk dengan aman — menggantikan class UploadHelper manual di studi kasus PHP+MySQL.
Form upload
<form method="POST" enctype="multipart/form-data">
@csrf
<input type="file" name="gambar" accept="image/*">
@error('gambar') <p>{{ $message }}</p> @enderror
</form>Validasi dan simpan file
public function rules(): array {
return [
// ... aturan lain ...
'gambar' => 'nullable|image|max:2048', // max dalam KB — 2048 = 2MB
];
}public function store(StoreProdukRequest $request) {
$data = $request->validated();
if ($request->hasFile('gambar')) {
// otomatis: nama file acak, disimpan di storage/app/public/produk
$data['gambar'] = $request->file('gambar')->store('produk', 'public');
}
Produk::create($data);
return redirect()->route('produk.index');
}Catatan — Satu baris ->store('produk', 'public') menggantikan seluruh isi class UploadHelper di studi kasus Materi 12: validasi tipe file sudah ditangani aturan image, nama file otomatis diacak oleh Laravel (bukan nama asli dari pengguna — perlindungan yang sama seperti Materi 14 handbook PHP), dan hasilnya langsung berupa path yang bisa disimpan ke database.
Symbolic link: syarat supaya file bisa diakses browser
php artisan storage:linkHati-hati — File yang di-upload tersimpan di storage/app/public/, yang tidak otomatis bisa diakses lewat browser (demi keamanan). storage:link membuat semacam "jalan pintas" dari public/storage menuju folder itu. Tanpa menjalankan perintah ini, gambar yang di-upload tidak akan tampil sama sekali di browser — kesalahan yang sangat umum bagi pemula.
Menampilkan gambar
@if ($p->gambar)
<img src="{{ asset('storage/' . $p->gambar) }}" width="80">
@else
<span>Tidak ada gambar</span>
@endifMenghapus file lama saat update
use Illuminate\Support\Facades\Storage;
public function update(UpdateProdukRequest $request, Produk $produk) {
$data = $request->validated();
if ($request->hasFile('gambar')) {
if ($produk->gambar) {
Storage::disk('public')->delete($produk->gambar); // buang file lama, jangan menumpuk
}
$data['gambar'] = $request->file('gambar')->store('produk', 'public');
}
$produk->update($data);
return redirect()->route('produk.index');
}Latihan — Tambahkan upload gambar ke form produk (create dan edit), lengkap dengan validasi image|max:2048. Jangan lupa php artisan storage:link. Uji: upload gambar valid, upload file bukan gambar (harus ditolak), dan update produk dengan gambar baru (pastikan gambar lama terhapus dari storage/app/public/produk).
Materi 21 · Modul 3
Mass assignment dan keamanan Eloquent
Melengkapi checklist keamanan dari handbook PHP dengan hal-hal spesifik Eloquent yang perlu diperhatikan.
Bahaya mass assignment tanpa $fillable
// bayangkan tabel users punya kolom "peran" (admin/kasir)
// dan controller register menerima SEMUA input mentah:
User::create($request->all());
// kalau seseorang menambahkan field tersembunyi di form:
// <input type="hidden" name="peran" value="admin">
// ... dan $fillable TIDAK dibatasi, dia bisa mendaftar sebagai admin!class User extends Authenticatable {
protected $fillable = ['name', 'email', 'password'];
// "peran" SENGAJA tidak dimasukkan — tidak bisa diisi lewat mass assignment
// kalau perlu diubah, lakukan EKSPLISIT: $user->peran = 'admin'; $user->save();
}Hati-hati — Ini adalah versi Eloquent dari peringatan whitelist: true di handbook NestJS dan $fillable yang disinggung di handbook Go — field sensitif (peran, saldo, status verifikasi) tidak boleh masuk $fillable, supaya tidak bisa diubah lewat input form yang dimanipulasi. Alternatif $guarded = [] (mengizinkan semua) sangat berbahaya kalau dipakai sembarangan — hindari kecuali kamu benar-benar memvalidasi ketat semua field secara terpisah.
Checklist keamanan Laravel
| Item | Status di Laravel |
|---|---|
| Query aman dari SQL injection | ✅ Otomatis — Eloquent & query builder selalu prepared statement (Materi 8) |
| Output di-escape dari XSS | ✅ Otomatis — {{ }} di Blade (Materi 4) |
| CSRF di semua form | ✅ Otomatis lewat @csrf, dicek middleware bawaan (Materi 13) |
| Password di-hash | ✅ Otomatis lewat Breeze/Auth::attempt() (Materi 17) |
| Mass assignment terkontrol | ⚠️ Wajib diatur manual lewat $fillable — TIDAK otomatis aman |
| Upload file divalidasi | ⚠️ Wajib pasang aturan image/mimes sendiri (Materi 20) |
| Otorisasi per aksi/data | ⚠️ Wajib ditulis lewat Gate/Policy (Materi 19) — Laravel tidak menebak aturan bisnismu |
Catatan — Poin penting: Laravel membuat banyak hal aman secara default, tapi bukan berarti kamu boleh berhenti berpikir soal keamanan. Prinsip yang sama dari handbook PHP Materi 15 dan 29 tetap berlaku sepenuhnya — framework menghilangkan pekerjaan berulang, bukan tanggung jawab berpikir soal siapa boleh melakukan apa.
Environment produksi
APP_ENV=production
APP_DEBUG=false # WAJIB false — sama seperti display_errors di handbook PHP Materi 10Hati-hati — APP_DEBUG=true di produksi menampilkan halaman error super detail (nama file, baris kode, bahkan isi variabel) ke siapa saja yang mengunjungi halaman error — kebocoran informasi yang serius. Selalu false di server produksi, persis alasan yang sama dengan mematikan display_errors di handbook PHP.
Latihan — Audit model Produk, Kategori, dan User di project latihanmu — pastikan $fillable hanya berisi field yang memang boleh diisi dari form publik. Coba simulasikan serangan: kirim POST request (lewat Postman) dengan field tambahan yang tidak ada di form aslimu, pastikan field itu tidak tersimpan.
Materi 22 · Modul 4
Membangun API
Menyajikan data sebagai JSON, bukan HTML — untuk dikonsumsi frontend seperti React (dibahas di handbook terpisah) atau aplikasi mobile.
Route API terpisah dari web
use App\Http\Controllers\Api\ProdukController;
Route::apiResource('produk', ProdukController::class);
// apiResource = seperti resource biasa, TAPI tanpa route create/edit
// (karena API tidak butuh "halaman form", cuma endpoint data)Semua route di routes/api.php otomatis diberi prefix /api dan dirancang untuk merespons JSON, bukan HTML — persis konsep public/api/produk.php di handbook PHP Materi 26, tapi dengan konvensi Laravel.
Controller API dan API Resource
php artisan make:controller Api/ProdukController --api
php artisan make:resource ProdukResource<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProdukResource extends JsonResource {
public function toArray($request): array {
return [
'id' => $this->id,
'nama' => $this->nama,
'harga' => $this->harga,
'harga_format' => $this->harga_format, // accessor dari Materi 11 tetap terpakai
'kategori' => $this->whenLoaded('kategori', fn () => [
'id' => $this->kategori->id,
'nama' => $this->kategori->nama,
]),
];
}
}use App\Http\Resources\ProdukResource;
public function index() {
$produk = Produk::with('kategori')->paginate(10);
return ProdukResource::collection($produk);
// otomatis membalas: { "data": [...], "links": {...}, "meta": {...} }
}
public function store(StoreProdukRequest $request) {
$produk = Produk::create($request->validated());
return (new ProdukResource($produk))
->response()
->setStatusCode(201);
}Catatan — Perhatikan bentuk respons { "data": [...], "meta": {...} } — ini persis kontrak API { data, meta } yang dibangun konsisten di seluruh seri handbook Node.js, Go, dan React. API Resource adalah cara Laravel mengontrol dengan tepat field apa saja yang keluar ke API (menyembunyikan password, kolom internal, dll) — setara konsep API Resource di Laravel yang sudah disinggung sebagai perbandingan di handbook Go Materi 12.
Menangani error API dengan format konsisten
$exceptions->render(function (\Illuminate\Validation\ValidationException $e, $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Validasi gagal',
'errors' => $e->errors(),
], 422);
}
});Ini persis bentuk error 422 { message, errors: {...} } yang konsisten dipakai di seluruh seri handbook backend — Laravel bahkan sudah menghasilkan format ini secara otomatis untuk request yang meng-expect JSON, tanpa kamu perlu menulis handler khusus seperti di atas untuk kasus dasar.
Latihan — Buat Api/ProdukController dengan method index dan store, plus ProdukResource. Uji lewat Postman atau curl: GET /api/produk harus membalas JSON dengan struktur { data, links, meta }, dan POST dengan data tidak valid harus membalas status 422 dengan detail error.
Materi 23 · Modul 4
Sanctum: autentikasi untuk API
Login berbasis session (Materi 17) bekerja untuk halaman web biasa — API butuh cara berbeda: token.
Kenapa API butuh token, bukan session
Session mengandalkan cookie yang dikirim browser otomatis di setiap request — cocok untuk halaman web biasa. Tapi aplikasi mobile, atau frontend terpisah (seperti React yang dibahas di handbook lain, berjalan di domain berbeda) tidak selalu punya cookie yang sama. Solusinya: token — string acak yang dikirim manual di header tiap request sebagai bukti identitas.
Memasang Sanctum
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrateuse Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable {
use HasApiTokens;
}Login API: menghasilkan token
public function login(Request $request) {
$request->validate(['email' => 'required|email', 'password' => 'required']);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json(['message' => 'Email atau password salah'], 401);
}
$token = $user->createToken('mobile-app')->plainTextToken;
return response()->json([
'user' => $user,
'accessToken' => $token, // klien harus MENYIMPAN token ini
]);
}Catatan — Bentuk respons { user, accessToken } ini persis kontrak login yang konsisten dipakai di seluruh seri handbook backend (Node.js, Go) — supaya frontend React yang sama bisa terhubung ke backend mana pun tanpa banyak penyesuaian.
Melindungi route API dengan token
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('produk', ProdukController::class);
Route::get('/user', fn (Request $request) => $request->user());
});// setiap request ke route terproteksi, sertakan header ini:
Authorization: Bearer <token yang didapat saat login>Catatan — auth:sanctum adalah middleware yang memeriksa header Authorization pada tiap request — mirip konsep Auth::wajibLogin() di studi kasus PHP+MySQL, tapi berbasis token, bukan session/cookie. Tanpa token yang valid, request ditolak dengan status 401.
Latihan — Pasang Sanctum, buat endpoint login API yang menghasilkan token. Lindungi Route::apiResource('produk', ...) dengan auth:sanctum. Uji lewat Postman: coba akses tanpa token (harus 401), lalu dengan token dari hasil login (harus berhasil).
Materi 24 · Modul 4
Queue dan Job
Menjalankan pekerjaan berat di latar belakang — supaya pengguna tidak menunggu lama untuk hal yang tidak perlu ditunggu langsung.
Masalah yang diselesaikan queue
Bayangkan setelah checkout, aplikasi harus mengirim email konfirmasi yang butuh 3 detik untuk terkirim. Kalau ditulis langsung di controller, pengguna menunggu 3 detik ekstra sebelum halaman berikutnya muncul — padahal mereka tidak perlu tahu persis kapan email itu selesai terkirim. Queue memindahkan pekerjaan seperti ini ke "antrean" yang diproses terpisah, di belakang layar.
Membuat Job
php artisan make:job KirimEmailKonfirmasi<?php
namespace App\Jobs;
use App\Models\Pesanan;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class KirimEmailKonfirmasi implements ShouldQueue {
use Dispatchable, Queueable;
public function __construct(public Pesanan $pesanan) {}
public function handle(): void {
// logika kirim email — dijalankan TERPISAH dari request asli
Mail::to($this->pesanan->user->email)
->send(new \App\Mail\PesananDiterima($this->pesanan));
}
}$pesanan = Pesanan::create([/* ... */]);
KirimEmailKonfirmasi::dispatch($pesanan);
// baris ini SELESAI SEKETIKA — tidak menunggu email benar-benar terkirim
return redirect()->route('pesanan.show', $pesanan);
// pengguna langsung diarahkan, TANPA menunggu proses kirim emailCatatan — dispatch() tidak langsung menjalankan handle() — dia mencatat job itu ke tabel jobs (Materi 22 konsep tabel dari handbook MySQL, dipakai untuk keperluan berbeda di sini) dan mengembalikan kontrol ke controller seketika. Proses worker terpisah yang benar-benar menjalankan handle()-nya belakangan.
Menjalankan queue worker
QUEUE_CONNECTION=database # untuk belajar; produksi biasanya pakai Redisphp artisan queue:table # buat migration tabel jobs (sekali saja)
php artisan migrate
php artisan queue:work # jalankan worker — proses job satu per satu, terus-menerusHati-hati — queue:work harus terus berjalan di server (biasanya diawasi supervisor seperti supervisord di produksi, mirip cara systemd menjaga service tetap hidup di handbook Go Materi 25) — kalau proses ini berhenti, job yang masuk antrean akan menumpuk tanpa pernah diproses. Saat development, cukup jalankan manual di terminal terpisah selagi mengembangkan fitur.
Latihan — Buat job sederhana CatatLogAktivitas yang menulis pesan ke storage/logs/laravel.log lewat Log::info(), dengan jeda buatan sleep(3) untuk mensimulasikan pekerjaan lambat. Panggil dispatch()-nya dari sebuah route, amati bahwa halaman merespons seketika (tanpa menunggu 3 detik), lalu cek log file untuk memastikan job benar-benar dijalankan oleh queue:work beberapa saat kemudian.
Materi 25 · Modul 4
Event dan Listener
Memisahkan "apa yang terjadi" dari "apa yang harus dilakukan setelahnya" — kode yang lebih longgar terhubung dan mudah diperluas.
Masalah yang diselesaikan event
Bayangkan saat checkout selesai, kamu ingin: kirim email (Materi 24), catat ke log aktivitas, dan update statistik penjualan. Menulis semuanya langsung di CheckoutController membuat controller itu tahu terlalu banyak hal yang sebenarnya tidak berkaitan langsung dengan "checkout" itu sendiri. Event membiarkan controller cukup bilang "pesanan sudah dibuat" — bagian lain aplikasi yang tertarik dengan kejadian itu bisa "mendengarkan" dan bereaksi sendiri.
Membuat Event dan Listener
php artisan make:event PesananDibuat
php artisan make:listener KirimEmailSaatPesananDibuat --event=PesananDibuat
php artisan make:listener CatatLogSaatPesananDibuat --event=PesananDibuat<?php
namespace App\Events;
use App\Models\Pesanan;
use Illuminate\Foundation\Events\Dispatchable;
class PesananDibuat {
use Dispatchable;
public function __construct(public Pesanan $pesanan) {}
}class KirimEmailSaatPesananDibuat {
public function handle(PesananDibuat $event): void {
KirimEmailKonfirmasi::dispatch($event->pesanan); // gabung dengan job Materi 24
}
}protected $listen = [
PesananDibuat::class => [
KirimEmailSaatPesananDibuat::class,
CatatLogSaatPesananDibuat::class,
// tambah listener baru di sini kapan pun, TANPA menyentuh controller
],
];$pesanan = Pesanan::create([/* ... */]);
PesananDibuat::dispatch($pesanan);
// controller TIDAK TAHU dan TIDAK PEDULI apa saja yang terjadi setelah ini
return redirect()->route('pesanan.show', $pesanan);Catatan — Keuntungan nyata: kalau nanti kamu ingin menambah reaksi baru (misalnya kirim notifikasi WhatsApp saat pesanan dibuat), cukup buat listener baru dan daftarkan — CheckoutController tidak perlu diubah sama sekali. Ini prinsip yang sama dengan interface Notifikasi di handbook PHP Materi 19 (kode bergantung pada kontrak, bukan implementasi spesifik), diterapkan di level yang lebih besar.
Latihan — Buat event PesananDibuat dan dua listener: satu yang memanggil job email dari Materi 24, satu lagi yang menulis log sederhana. Daftarkan keduanya. Panggil PesananDibuat::dispatch($pesanan) dari route uji coba, dan pastikan kedua listener berjalan.
Materi 26 · Modul 4
Testing dengan Pest/PHPUnit
Laravel dirancang untuk mudah diuji — database uji terpisah, factory, dan assertion siap pakai.
Membuat test
php artisan make:test ProdukTest<?php
namespace Tests\Feature;
use App\Models\Produk;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ProdukTest extends TestCase {
use RefreshDatabase; // database di-reset BERSIH sebelum tiap test — aman, terisolasi
public function test_halaman_daftar_produk_bisa_diakses(): void {
$user = User::factory()->create();
Produk::factory()->count(3)->create();
$response = $this->actingAs($user)->get('/produk');
$response->assertStatus(200);
$response->assertSee('Daftar Produk');
}
public function test_tamu_tidak_bisa_akses_halaman_produk(): void {
$response = $this->get('/produk'); // TANPA login
$response->assertRedirect('/login'); // middleware auth (Materi 18) bekerja
}
public function test_produk_baru_bisa_disimpan(): void {
$user = User::factory()->create();
$kategori = Kategori::factory()->create();
$this->actingAs($user)->post('/produk', [
'nama' => 'Kaos Uji',
'harga' => 85000,
'stok' => 10,
'kategori_id' => $kategori->id,
]);
$this->assertDatabaseHas('produk', ['nama' => 'Kaos Uji']);
}
public function test_validasi_menolak_harga_negatif(): void {
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/produk', ['nama' => 'X', 'harga' => -1000]);
$response->assertSessionHasErrors('harga');
}
}Catatan — RefreshDatabase otomatis membuat database bersih sebelum tiap test — menyelesaikan peringatan di handbook studi kasus PHP+MySQL Materi 19 soal "test butuh database terpisah". actingAs($user) mensimulasikan pengguna yang sudah login, dan factory (Materi 10) membuat data uji dalam satu baris. Bandingkan kepraktisan ini dengan test manual di studi kasus PHP+MySQL Materi 19.
Menjalankan test
php artisan test
# PASS Tests\Feature\ProdukTest
# ✓ halaman daftar produk bisa diakses
# ✓ tamu tidak bisa akses halaman produk
# ✓ produk baru bisa disimpan
# ✓ validasi menolak harga negatifTest untuk Eloquent model langsung
public function test_accessor_harga_format_benar(): void {
$produk = Produk::factory()->create(['harga' => 85000]);
$this->assertEquals('Rp 85.000', $produk->harga_format);
}
public function test_scope_tersedia_hanya_ambil_stok_positif(): void {
Produk::factory()->create(['stok' => 5]);
Produk::factory()->create(['stok' => 0]);
$this->assertCount(1, Produk::tersedia()->get());
}Ini menguji langsung fitur accessor (Materi 11) dan scope (Materi 11) yang kamu buat sebelumnya — sama seperti prinsip "uji logika bisnis penting" di handbook PHP Materi 28.
Latihan — Tulis test lengkap untuk ProdukController: bisa diakses saat login, ditolak saat belum login, bisa menyimpan data valid, menolak data tidak valid. Tulis juga satu test untuk scope Eloquent yang kamu buat di Materi 11. Jalankan php artisan test dan pastikan semua lulus.
Materi 27 · Modul 5
Caching dan optimasi
Menghindari kerja berulang yang tidak perlu — data yang jarang berubah tidak perlu di-query ulang setiap request.
Cache sederhana
use Illuminate\Support\Facades\Cache;
// remember: ambil dari cache kalau ada, kalau tidak — jalankan closure, SIMPAN hasilnya
$kategori = Cache::remember('daftar-kategori', 3600, function () {
return Kategori::all(); // query INI hanya jalan sekali per jam, bukan tiap request
});Catatan — Angka 3600 adalah durasi cache dalam detik (1 jam). Selama itu, permintaan berikutnya untuk 'daftar-kategori' langsung mengambil dari cache — tanpa menyentuh database sama sekali. Cocok untuk data yang jarang berubah (kategori, pengaturan situs) tapi sering diakses (misalnya dropdown yang muncul di banyak halaman).
Menghapus cache saat data berubah
public function store(Request $request) {
Kategori::create($request->validated());
Cache::forget('daftar-kategori'); // buang cache lama — supaya data baru "terlihat"
return redirect()->route('kategori.index');
}Hati-hati — Ini masalah klasik caching: kalau lupa menghapus (invalidate) cache saat data yang mendasarinya berubah, pengguna akan melihat data basi sampai cache-nya kedaluwarsa sendiri. Setiap tempat yang menyimpan cache harus punya pasangan yang jelas — di mana dan kapan cache itu dihapus.
Cache konfigurasi dan route untuk produksi
php artisan config:cache # gabungkan semua config jadi satu file — baca .env cuma sekali
php artisan route:cache # percepat pencocokan route, terutama untuk banyak route
php artisan view:cache # pre-compile semua Blade jadi PHP murniHati-hati — Setelah config:cache dijalankan, perubahan di file .env tidak akan terbaca sampai kamu menjalankan php artisan config:clear atau config:cache ulang. Ini jebakan umum: mengubah .env di server produksi tapi lupa cache-nya belum di-refresh, sehingga perubahan seolah "tidak ngaruh".
Menghindari N+1 sebagai bentuk optimasi termudah
Sebelum buru-buru menambah cache, ingat kembali Materi 9: memastikan tidak ada masalah N+1 (lewat with()) sering memberi peningkatan performa yang jauh lebih besar daripada caching — dan tidak punya risiko data basi sama sekali. Urutan yang disarankan: perbaiki query dulu (index yang tepat, eager loading), baru pertimbangkan cache untuk data yang memang mahal dihitung ulang atau jarang berubah.
Latihan — Tambahkan Cache::remember() untuk daftar kategori (dipakai di dropdown form produk). Pastikan cache-nya dihapus di method store, update, dan destroy pada KategoriController. Uji: tambah kategori baru, pastikan langsung muncul di dropdown tanpa harus menunggu cache kedaluwarsa.
Materi 28 · Modul 5 · Penutup
Deployment dan penutup
Membawa aplikasi Laravel ke server sungguhan, dan merangkum keseluruhan perjalanan.
Checklist sebelum go-live
- ✅
APP_ENV=productiondanAPP_DEBUG=false(Materi 21). - ✅
APP_KEYsudah di-generate (php artisan key:generate) — dipakai untuk enkripsi session dan lainnya. - ✅ Database produksi dibuat, migration dijalankan (
php artisan migrate --force). - ✅
composer install --no-dev --optimize-autoloader— persis prinsip yang sama dengan handbook PHP Materi 30. - ✅
php artisan config:cache,route:cache,view:cache(Materi 27). - ✅ Document root web server diarahkan ke folder
public/— sama seperti struktur manual di handbook PHP Materi 25. - ✅
php artisan storage:linksudah dijalankan, supaya file upload bisa diakses (Materi 20). - ✅ Queue worker (
queue:work) berjalan di bawah supervisor kalau memakai job (Materi 24). - ✅ HTTPS aktif — sama pentingnya seperti dibahas di handbook PHP Materi 29.
- ✅ Backup database terjadwal — prinsip yang sama seperti handbook MySQL Materi 25, cuma sekarang datanya lebih kompleks (dengan migration history, dll).
Alur deploy sederhana
1. git pull (atau lewat CI/CD)
2. composer install --no-dev --optimize-autoloader
3. php artisan migrate --force
4. php artisan config:cache && route:cache && view:cache
5. php artisan queue:restart # worker ambil kode terbaru
6. Restart PHP-FPM kalau perluYang sudah kamu kuasai
| Modul | Isi |
|---|---|
| Modul 0 | Instalasi, routing, Blade dasar, Artisan CLI. |
| Modul 1 | Migration, Eloquent model, query builder, relasi, seeder/factory, accessor/mutator/scope. |
| Modul 2 | Controller, Form Request, layout/component Blade, session/flash, studi kasus CRUD lengkap. |
| Modul 3 | Autentikasi (Breeze), middleware, Gate/Policy, file upload, keamanan Eloquent. |
| Modul 4 | API Resource, Sanctum, queue/job, event/listener, testing. |
| Modul 5 | Caching, praktik produksi, deployment. |
Apa yang sebenarnya berubah dari handbook sebelumnya
Kalau ditinjau ulang, hampir tidak ada konsep baru di handbook ini yang belum pernah disinggung — prepared statement, validasi, autentikasi, relasi database, routing, N+1, caching, semuanya sudah dibahas di handbook PHP dan MySQL. Yang berubah adalah seberapa banyak yang perlu kamu tulis sendiri untuk mendapatkan hasil yang sama. Ini alasan sesungguhnya kenapa framework berharga: bukan karena dia "lebih pintar" darimu, tapi karena dia sudah menyelesaikan ribuan keputusan berulang yang sebelumnya kamu buat sendiri di setiap project.
Langkah selanjutnya
Ke mana setelah ini — Dari sini, arah yang wajar: mendalami Livewire atau Inertia.js untuk membangun UI yang lebih interaktif tanpa membangun API terpisah, mempelajari Filament untuk membangun panel admin secara sangat cepat (mirip yang disinggung sebagai perbandingan di handbook Go Materi 31), atau menghubungkan Laravel sebagai backend API murni ke frontend React yang dibahas di handbook terpisah — memakai persis kontrak { data, meta } dan pola autentikasi token yang sudah dipraktikkan di Modul 4 di sini.
Dari Route::get('/', fn () => 'Halo dari Laravel!') di Materi 2, sampai checkout dengan event, job, dan API bertoken di Modul 4 — kamu sudah menempuh jalur penuh dari framework kosong sampai aplikasi produksi yang lengkap. Semua fondasi dari handbook PHP dan MySQL tidak pernah benar-benar ditinggalkan — mereka cuma sekarang bekerja di balik layar, dijalankan Laravel atas namamu.