Materi 1 · Modul 0
Kenalan sama PHP
Bahasa yang menjalankan sebagian besar website di dunia. Sebelum ngoding, pahami dulu bentuk kerjanya yang khas.
PHP itu apa
PHP adalah bahasa pemrograman yang dijalankan di server, bukan di browser pengunjung. Tugas utamanya: menyusun halaman HTML atau data untuk dikirim ke browser. Saat kamu membuka sebuah website, kalau alamatnya berakhiran .php — atau di balik layar memakai PHP — maka ada program PHP di server yang berjalan sesaat, menghasilkan halaman, lalu mengirimnya ke kamu.
Alur satu permintaan
Ini bentuk kerja yang paling perlu kamu pahami di awal, karena berbeda dari banyak bahasa lain.
1. Pengunjung membuka https://tokosaya.com/produk.php
2. Server menjalankan produk.php DARI AWAL
3. Skrip menghasilkan HTML (atau JSON)
4. Server mengirim hasilnya ke browser
5. Skrip SELESAI dan semua variabelnya DIHAPUS
Permintaan berikutnya mengulang dari langkah 1 — dari nol lagi.Catatan — Poin terpenting: setiap permintaan mulai dari nol. PHP tidak "mengingat" apa pun antar permintaan dengan sendirinya. Kalau kamu mau menyimpan sesuatu (barang di keranjang, siapa yang sedang login), itu harus disimpan di tempat yang bertahan — database, file, atau session. Ini yang bikin PHP sederhana dipahami: satu file, jalan sekali, selesai.
Kenapa PHP masih relevan
- Mudah dijalankan di mana saja. Hampir semua layanan hosting murah, bahkan yang paling sederhana, sudah mendukung PHP tanpa setup rumit.
- Dirancang khusus untuk web. Menerima form, membaca data yang dikirim browser, menghasilkan HTML — semua itu bawaan bahasanya.
- Ekosistem besar dan matang. WordPress (yang menjalankan sebagian besar website dunia) memakai PHP, begitu juga banyak framework modern.
- Cepat memberi hasil. Kamu bisa membuat halaman dinamis pertamamu dalam hitungan menit, tanpa banyak alat pendukung.
Yang akan kamu pelajari di handbook ini
- Modul 0–1 — dasar bahasa: variabel, tipe data, kondisi, perulangan, fungsi, array.
- Modul 2 — PHP untuk web: menerima form, session, cookie, keamanan input.
- Modul 3 — pemrograman berorientasi objek (OOP): class, object, inheritance.
- Modul 4 — PHP modern: Composer, namespace, error handling, struktur project.
- Modul 5 — topik lanjutan: keamanan, API, dan praktik produksi.
Untuk urusan database (MySQL), ada handbook terpisah. Setelah keduanya selesai, ada handbook studi kasus yang menggabungkan PHP dan MySQL jadi satu aplikasi utuh.
Versi PHP — Handbook ini memakai PHP 8.1 ke atas. Kalau kamu menemukan tutorial lama yang memakai fungsi seperti mysql_query() atau kode tanpa tipe, itu gaya PHP versi lama yang sudah tidak dianjurkan. Kita pakai gaya modern sejak awal supaya kamu tidak perlu "belajar ulang" nanti.
Materi 2 · Modul 0
Setup dan file PHP pertama
Menyiapkan lingkungan kerja, dan menjalankan skrip pertamamu.
Yang perlu dipasang
- PHP itu sendiri. Cara paling mudah untuk belajar: pasang paket all-in-one yang berisi PHP + server + database sekaligus — Laragon (Windows, paling direkomendasikan), XAMPP (semua OS), atau MAMP (macOS). Ketiganya gratis.
- Editor kode. VS Code, dengan ekstensi PHP Intelephense untuk autocomplete dan deteksi error.
- Browser apa saja untuk melihat hasilnya.
Catatan — Paket seperti Laragon menyediakan PHP, web server (Apache/Nginx), dan MySQL dalam satu instalasi. Untuk sekarang kita cukup pakai PHP-nya. Kalau kamu ingin, kamu juga bisa cek versi PHP di terminal dengan php -v.
Menjalankan tanpa server dulu
Cara tercepat mencoba PHP: lewat terminal. Buat file coba.php:
<?php
echo "Halo dari PHP!";
php coba.php
# output: Halo dari PHP!
# atau jalankan server bawaan PHP untuk mencoba di browser
php -S localhost:8000
# lalu buka http://localhost:8000/coba.phpServer bawaan (php -S) sangat berguna untuk belajar: tidak perlu Apache atau konfigurasi apa pun, cukup satu perintah dan folder-mu langsung bisa diakses lewat browser.
Anatomi file PHP
<?php
// Semua kode PHP ditulis di antara tag pembuka <?php ...
echo "Ini dijalankan oleh PHP";
// ... dan tag penutup ?> (boleh dihilangkan kalau file isinya PHP saja)Hati-hati — Untuk file yang isinya PHP saja (tanpa HTML setelahnya), konvensi modern adalah tidak menulis tag penutup ?> di akhir file. Alasannya: spasi atau baris kosong yang tak sengaja tertinggal setelah ?> bisa merusak halaman, terutama saat mengirim header atau file. Jadi biasakan menghilangkannya.
Menyisipkan PHP di dalam HTML
Kekuatan asli PHP: dia bisa berpindah-pindah antara HTML biasa dan kode. Apa pun di luar tag <?php ?> dikirim apa adanya ke browser.
<!DOCTYPE html>
<html>
<body>
<h1>Selamat datang</h1>
<p>Hari ini tanggal <?php echo date("d F Y"); ?></p>
<?php if (date("H") < 12): ?>
<p>Selamat pagi!</p>
<?php else: ?>
<p>Selamat siang!</p>
<?php endif; ?>
</body>
</html>Ada singkatan yang berguna untuk menampilkan nilai: <?= $nama ?> sama dengan <?php echo $nama; ?>. Sering dipakai saat mencampur PHP di dalam HTML.
Komentar
<?php
// komentar satu baris
# ini juga komentar satu baris
/*
komentar
beberapa baris
*/
/**
* Komentar dokumentasi (docblock).
* Dipakai untuk menjelaskan fungsi atau class.
*/Latihan — Buat file profil.php yang menampilkan halaman HTML sederhana berisi namamu, kota, dan tahun sekarang (pakai date("Y")). Jalankan dengan php -S localhost:8000 dan buka di browser.
Materi 3 · Modul 0
Variabel dan tipe data
Cara menyimpan dan mengolah nilai. Fondasi dari semua kode berikutnya.
Variabel
Variabel di PHP selalu diawali tanda $. Kamu tidak perlu menyebutkan tipenya — PHP menebaknya dari nilai yang kamu berikan.
<?php
$nama = "Kaos Onlenkan";
$harga = 85000;
$aktif = true;
$diskon = 0.15;
// nama variabel: huruf/underscore di awal, boleh angka setelahnya
// case-sensitive: $nama dan $Nama itu BERBEDA
echo $nama; // Kaos Onlenkan
echo $harga * 2; // 170000Tipe data dasar
<?php
$teks = "Halo"; // string
$bulat = 42; // int (bilangan bulat)
$pecahan = 3.14; // float (bilangan desimal)
$benar = true; // bool (true / false)
$kosong = null; // null (tidak ada nilai)
$daftar = ["a", "b", "c"]; // array (dibahas terpisah)
// cek tipe sebuah variabel
echo gettype($bulat); // integer
var_dump($pecahan); // float(3.14) — sangat berguna saat debuggingCatatan — var_dump() adalah alat debugging paling sering dipakai di PHP. Dia menampilkan tipe dan nilai sebuah variabel. Kalau kamu bingung kenapa kode tidak berjalan seperti harapan, var_dump() variabelnya dulu — sering langsung terlihat masalahnya.
String
<?php
$nama = "Nus";
// petik ganda: variabel di dalamnya IKUT diproses
echo "Halo, $nama!"; // Halo, Nus!
echo "Total: Rp {$harga}"; // kurung kurawal biar jelas batasnya
// petik tunggal: ditampilkan APA ADANYA
echo 'Halo, $nama!'; // Halo, $nama! (tidak diproses)
// gabung string dengan titik
echo "Halo, " . $nama . "!";
// fungsi string yang sering dipakai
strlen($nama); // 3 — jumlah karakter
strtoupper($nama); // NUS
strtolower($nama); // nus
trim(" spasi "); // "spasi" — buang spasi di ujung
str_replace(" ", "-", $nama); // ganti spasi jadi strip
substr($nama, 0, 2); // "Nu" — potong dari posisi 0, ambil 2
str_contains($nama, "us"); // true — apakah memuat "us"?Angka dan operasi matematika
<?php
$a = 10; $b = 3;
echo $a + $b; // 13
echo $a - $b; // 7
echo $a * $b; // 30
echo $a / $b; // 3.333...
echo $a % $b; // 1 — sisa bagi (modulo)
echo $a ** $b; // 1000 — pangkat
// fungsi angka
round(3.7); // 4
floor(3.7); // 3 — dibulatkan ke bawah
ceil(3.2); // 4 — dibulatkan ke atas
abs(-5); // 5 — nilai mutlak
max(4, 9, 2); // 9
number_format(85000, 0, ",", "."); // "85.000" — format rupiahHati-hati — Jangan simpan uang sebagai float untuk perhitungan penting. 0.1 + 0.2 di PHP tidak sama persis dengan 0.3 karena keterbatasan bilangan desimal di komputer. Untuk uang, simpan sebagai int dalam satuan terkecil (rupiah penuh), atau pakai fungsi khusus seperti bcadd() kalau butuh presisi tinggi.
Konstanta
<?php
// nilai yang tidak akan berubah — tanpa tanda $
const PPN = 0.11;
define("NAMA_TOKO", "Onlenkan Store");
echo NAMA_TOKO; // Onlenkan Store
echo 100000 * PPN; // 11000Latihan — Buat variabel $harga (85000) dan $qty (3). Hitung subtotal, tambahkan PPN 11%, lalu tampilkan totalnya dengan format rupiah pakai number_format(). Contoh hasil: "Total: Rp 283.050".
Materi 4 · Modul 0
Kondisi dan operator
Membuat program mengambil keputusan berdasarkan kondisi tertentu.
Operator perbandingan
<?php
$a == $b // sama nilainya (tipe tidak dicek ketat)
$a === $b // sama nilai DAN sama tipe — pakai ini secara default
$a != $b // tidak sama
$a !== $b // tidak sama nilai atau tipe
$a > $b // lebih besar
$a <= $b // kurang dari atau sama denganHati-hati — Selalu utamakan === (tiga sama dengan), bukan ==. Dengan ==, PHP mencoba menyamakan tipe dulu, dan hasilnya bisa mengejutkan: 0 == "halo" pernah bernilai true di versi lama, dan "1" == 1 bernilai true. === memeriksa nilai dan tipe, jadi jauh lebih aman dan bisa diprediksi.
Operator logika
<?php
$a && $b // AND — keduanya harus true
$a || $b // OR — salah satu true saja cukup
!$a // NOT — kebalikannya
// contoh
if ($stok > 0 && $aktif === true) {
echo "Produk bisa dibeli";
}if / elseif / else
<?php
$nilai = 78;
if ($nilai >= 90) {
echo "A";
} elseif ($nilai >= 80) {
echo "B";
} elseif ($nilai >= 70) {
echo "C";
} else {
echo "Belum lulus";
}Operator ternary dan null coalescing
Cara singkat untuk kondisi sederhana. Sering dipakai, terutama saat menampilkan data.
<?php
// ternary: kondisi ? nilai_kalau_true : nilai_kalau_false
$status = ($stok > 0) ? "Tersedia" : "Habis";
// null coalescing (??): pakai nilai kedua kalau yang pertama null/tidak ada
$nama = $_GET["nama"] ?? "Tamu";
// kalau $_GET["nama"] tidak ada → pakai "Tamu", tanpa error
// null coalescing assignment
$config["tema"] ??= "terang"; // isi kalau belum adaCatatan — ?? (null coalescing) itu penyelamat saat berurusan dengan data dari form atau URL yang mungkin tidak ada. Tanpa itu, mengakses key yang tidak ada akan memunculkan peringatan. Kamu akan sangat sering memakainya di Modul 2.
switch dan match
<?php
// switch — untuk banyak kemungkinan dari satu nilai
switch ($status) {
case "lunas":
echo "Sudah dibayar";
break; // WAJIB, kalau lupa akan lanjut ke case berikutnya
case "pending":
echo "Menunggu pembayaran";
break;
default:
echo "Status tidak dikenal";
}
// match (PHP 8+) — lebih ringkas, pakai === , tidak perlu break
$label = match($status) {
"lunas" => "Sudah dibayar",
"pending" => "Menunggu pembayaran",
"batal", "gagal" => "Tidak berhasil", // beberapa nilai sekaligus
default => "Status tidak dikenal",
};match lebih aman daripada switch: dia memakai perbandingan ketat (===) dan tidak butuh break, jadi tidak ada risiko lupa yang bikin bug. Gunakan match kalau tiap cabang cuma menghasilkan sebuah nilai.
Latihan — Buat fungsi kecil yang menerima total belanja, lalu tentukan ongkir: gratis kalau ≥ 200.000, Rp 10.000 kalau ≥ 100.000, dan Rp 15.000 kalau di bawah itu. Coba dengan beberapa nilai berbeda.
Materi 5 · Modul 0
Perulangan
Menjalankan kode berulang kali — untuk memproses daftar, menampilkan tabel, dan banyak lagi.
for
<?php
// for (nilai awal; kondisi lanjut; perubahan tiap putaran)
for ($i = 1; $i <= 5; $i++) {
echo "Baris $i\n";
}
// Baris 1, Baris 2, ... Baris 5while dan do-while
<?php
// while — ulangi selama kondisi masih true
$stok = 5;
while ($stok > 0) {
echo "Sisa stok: $stok\n";
$stok--;
}
// do-while — jalankan sekali dulu, baru cek kondisi
$n = 0;
do {
echo "Jalan minimal sekali\n";
$n++;
} while ($n < 3);Hati-hati — Pastikan kondisi while pada akhirnya menjadi false. Kalau tidak, program terjebak dalam perulangan tak berujung dan server bisa menggantung. Selalu ada sesuatu di dalam loop yang mengubah kondisinya (seperti $stok-- di atas).
foreach — yang paling sering dipakai
Untuk memproses setiap item dalam sebuah array. Ini bentuk perulangan yang akan paling sering kamu tulis di PHP, terutama saat menampilkan data dari database.
<?php
$produk = ["Kaos", "Hoodie", "Topi"];
// ambil tiap nilai
foreach ($produk as $item) {
echo "- $item\n";
}
// ambil kunci (index) DAN nilai
foreach ($produk as $index => $item) {
echo "$index: $item\n"; // 0: Kaos, 1: Hoodie, ...
}
// array asosiatif (dibahas di materi berikutnya)
$harga = ["kaos" => 85000, "hoodie" => 210000];
foreach ($harga as $nama => $rp) {
echo "$nama: Rp $rp\n";
}Menampilkan tabel HTML dari data
Contoh nyata yang menggabungkan foreach dengan HTML — pola yang akan kamu pakai terus:
<?php
$produk = [
["nama" => "Kaos", "harga" => 85000],
["nama" => "Hoodie", "harga" => 210000],
];
?>
<table>
<tr><th>Nama</th><th>Harga</th></tr>
<?php foreach ($produk as $p): ?>
<tr>
<td><?= $p["nama"] ?></td>
<td>Rp <?= number_format($p["harga"], 0, ",", ".") ?></td>
</tr>
<?php endforeach; ?>
</table>Catatan — Perhatikan gaya foreach (...): ... endforeach; — ini alternatif tanda kurung kurawal yang lebih enak dibaca saat mencampur PHP dengan HTML. Sama fungsinya, tapi lebih jelas mana yang PHP mana yang HTML.
break dan continue
<?php
foreach ($produk as $p) {
if ($p["stok"] === 0) {
continue; // lewati item ini, lanjut ke berikutnya
}
if ($p["nama"] === "Selesai") {
break; // hentikan seluruh perulangan
}
echo $p["nama"];
}Latihan — Buat array berisi 5 produk (masing-masing punya nama, harga, stok). Pakai foreach untuk menampilkan hanya produk yang stoknya lebih dari 0, dalam bentuk tabel HTML dengan harga terformat rupiah.
Materi 6 · Modul 0
Array
Struktur data terpenting di PHP. Menyimpan banyak nilai dalam satu variabel.
Dua jenis array
<?php
// array berindeks — otomatis diberi nomor mulai dari 0
$produk = ["Kaos", "Hoodie", "Topi"];
echo $produk[0]; // Kaos
echo $produk[2]; // Topi
// array asosiatif — pakai "kunci" yang kamu tentukan sendiri
$orang = [
"nama" => "Nus",
"kota" => "Probolinggo",
"umur" => 28,
];
echo $orang["nama"]; // NusCatatan — Array asosiatif itu tulang punggung PHP untuk web. Setiap baris data dari database, setiap kiriman form, hampir semuanya berbentuk array asosiatif seperti ["nama" => ..., "harga" => ...]. Kuasai ini baik-baik.
Menambah dan mengubah
<?php
$produk = ["Kaos", "Hoodie"];
$produk[] = "Topi"; // tambah di akhir
array_push($produk, "Jaket"); // juga menambah di akhir
$produk[0] = "Kaos Polos"; // ubah nilai
unset($produk[1]); // hapus item
// array asosiatif
$orang["email"] = "nus@toko.com"; // tambah key baruFungsi array yang wajib dikuasai
<?php
$angka = [3, 1, 4, 1, 5, 9];
count($angka); // 6 — jumlah item
in_array(4, $angka); // true — apakah 4 ada?
array_search(4, $angka); // 2 — posisi 4
sort($angka); // urutkan menaik (mengubah array asli)
array_sum($angka); // jumlah semua isi
array_unique($angka); // buang duplikat
// map — ubah setiap item jadi sesuatu yang lain
$harga = [10000, 25000, 5000];
$kena = array_map(fn($h) => $h * 1.11, $harga);
// filter — ambil hanya yang lolos syarat
$mahal = array_filter($harga, fn($h) => $h > 10000);
// ambil satu kolom dari array of array
$produk = [["nama" => "Kaos"], ["nama" => "Topi"]];
$nama = array_column($produk, "nama"); // ["Kaos", "Topi"]
// keys dan values
array_keys($orang); // ["nama", "kota", "umur"]
array_values($orang); // ["Nus", "Probolinggo", 28]Array bertingkat
Array bisa berisi array lain. Inilah bentuk data yang akan kamu terima dari database: daftar baris, di mana tiap baris adalah array asosiatif.
<?php
$keranjang = [
["nama" => "Kaos", "harga" => 85000, "qty" => 2],
["nama" => "Hoodie", "harga" => 210000, "qty" => 1],
];
// akses: [index baris][kunci kolom]
echo $keranjang[0]["nama"]; // Kaos
// hitung total keseluruhan
$total = 0;
foreach ($keranjang as $item) {
$total += $item["harga"] * $item["qty"];
}
echo "Total: Rp " . number_format($total, 0, ",", ".");Latihan — Dari array $keranjang di atas, gunakan array_map untuk membuat array baru berisi subtotal tiap item (harga × qty), lalu array_sum untuk menjumlahkannya. Bandingkan hasilnya dengan cara foreach.
Materi 7 · Modul 1
Fungsi
Membungkus kode jadi satu unit yang bisa dipanggil berulang. Fondasi kode yang rapi.
Membuat dan memanggil fungsi
<?php
function sapa($nama) {
return "Halo, $nama!";
}
echo sapa("Nus"); // Halo, Nus!
echo sapa("Jasmine"); // Halo, Jasmine!
// return mengembalikan nilai. tanpa return, fungsi mengembalikan null.
function hitungTotal($harga, $qty) {
return $harga * $qty;
}
$total = hitungTotal(85000, 3); // 255000Tipe parameter dan tipe kembalian
PHP modern menganjurkan menuliskan tipe. Ini membuat kode lebih jelas dan menangkap kesalahan lebih awal.
<?php
declare(strict_types=1); // letakkan di baris pertama file — bikin tipe ketat
// int $harga = parameter harus integer
// : int di akhir = fungsi harus mengembalikan integer
function hitungTotal(int $harga, int $qty): int {
return $harga * $qty;
}
hitungTotal(85000, 3); // ✅ 255000
hitungTotal("85000", 3); // ❌ error dengan strict_types — string bukan int
// tipe yang boleh null: pakai ? di depan
function cariNama(int $id): ?string {
// mengembalikan string ATAU null
return $id === 1 ? "Nus" : null;
}Catatan — Tambahkan declare(strict_types=1); di baris pertama setiap file PHP-mu. Tanpa itu, PHP diam-diam mengubah "85000" jadi 85000. Dengan itu, tipe yang tidak cocok langsung memunculkan error — jauh lebih aman, dan menangkap bug sebelum sampai ke produksi.
Nilai default dan named arguments
<?php
// parameter dengan nilai default — boleh tidak diisi saat memanggil
function formatRupiah(int $angka, string $prefix = "Rp "): string {
return $prefix . number_format($angka, 0, ",", ".");
}
formatRupiah(85000); // Rp 85.000
formatRupiah(85000, "IDR "); // IDR 85.000
// named arguments (PHP 8+) — sebut nama parameternya, urutan bebas
formatRupiah(angka: 85000, prefix: "Rp. ");Fungsi dengan jumlah argumen bebas
<?php
// ...$angka mengumpulkan semua argumen jadi satu array
function jumlahkan(int ...$angka): int {
return array_sum($angka);
}
jumlahkan(1, 2, 3); // 6
jumlahkan(10, 20, 30, 40); // 100Arrow function dan fungsi anonim
<?php
// fungsi anonim (closure) — disimpan di variabel
$kali2 = function($n) {
return $n * 2;
};
echo $kali2(5); // 10
// arrow function — versi singkat untuk satu ekspresi
$kali2 = fn($n) => $n * 2;
// paling sering dipakai bersama array_map / array_filter
$harga = [10000, 25000];
$kena = array_map(fn($h) => $h * 1.11, $harga);Contoh nyata: kumpulan fungsi pembantu
<?php
declare(strict_types=1);
function rupiah(int $angka): string {
return "Rp " . number_format($angka, 0, ",", ".");
}
function bikinSlug(string $teks): string {
$teks = strtolower(trim($teks));
$teks = preg_replace('/[^a-z0-9]+/', '-', $teks);
return trim($teks, '-');
}
function potongTeks(string $teks, int $maks = 100): string {
if (strlen($teks) <= $maks) return $teks;
return substr($teks, 0, $maks) . "...";
}
// pakai di mana saja
echo rupiah(85000); // Rp 85.000
echo bikinSlug("Kaos Edisi Terbatas"); // kaos-edisi-terbatasLatihan — Buat fungsi hitungDiskon(int $harga, float $persen): int yang mengembalikan harga setelah diskon (dibulatkan). Tambahkan declare(strict_types=1) dan tipe pada semua parameter. Uji dengan beberapa nilai.
Materi 8 · Modul 1
Scope dan cara memecah file
Di mana variabel bisa diakses, dan cara memisahkan kode jadi beberapa file.
Scope variabel
Variabel di dalam fungsi terpisah dari variabel di luarnya. Ini disengaja — supaya fungsi tidak tanpa sengaja mengubah variabel lain.
<?php
$pesan = "di luar";
function coba() {
// $pesan dari luar TIDAK terlihat di sini
echo $pesan ?? "tidak ada"; // "tidak ada"
}
// cara yang benar: kirim lewat parameter
function cobaBenar(string $pesan) {
echo $pesan;
}
cobaBenar($pesan); // "di luar"Hati-hati — Ada kata kunci global untuk memaksa fungsi mengakses variabel luar, tapi hindari. Variabel global membuat kode sulit dilacak: kamu tidak bisa tahu dari mana sebuah nilai berasal. Selalu kirim data lewat parameter, dan kembalikan hasil lewat return. Itu membuat setiap fungsi bisa dipahami sendiri.
Memecah kode ke beberapa file
Saat project tumbuh, kamu tidak mau semua kode dalam satu file. PHP punya cara menyertakan file lain: require dan include.
<?php
require "helpers.php"; // sisipkan isi file. ERROR fatal kalau tidak ada.
require_once "helpers.php"; // sama, tapi cegah disisipkan dua kali
include "opsional.php"; // hanya WARNING kalau tidak ada, program lanjut
include_once "opsional.php";
// setelah require, semua fungsi & variabel dari file itu bisa dipakai
echo rupiah(85000); // fungsi dari helpers.php| Perintah | Kalau file tidak ada |
|---|---|
require | Program berhenti (fatal error). Pakai untuk file yang wajib ada. |
include | Cuma warning, program lanjut. Untuk file opsional. |
*_once | Versi yang mencegah file disisipkan lebih dari sekali. |
Catatan — Untuk hampir semua kasus, pakai require_once. Dia memastikan file wajib ada (menangkap salah ketik path lebih awal) dan mencegah error "fungsi didefinisikan dua kali" saat file yang sama tak sengaja disisipkan berulang. Di Modul 4 kita akan pakai Composer autoload yang membuat require manual jarang diperlukan.
Struktur file yang umum
project/
├── index.php ← halaman utama
├── includes/
│ ├── header.php ← bagian atas HTML yang dipakai semua halaman
│ ├── footer.php
│ └── helpers.php ← kumpulan fungsi pembantu
├── config.php ← pengaturan (koneksi database, dll)
└── produk.phpTemplate sederhana dengan include
Pola klasik PHP: pisahkan bagian HTML yang berulang (header, footer) jadi file terpisah, lalu sisipkan di tiap halaman.
<!DOCTYPE html>
<html>
<head>
<title><?= $judul ?? "Toko Saya" ?></title>
</head>
<body>
<nav>Menu di sini</nav><?php
$judul = "Daftar Produk";
require "includes/header.php";
?>
<h1>Produk Kami</h1>
<p>Isi halaman produk...</p>
<?php require "includes/footer.php"; ?>Dengan pola ini, kalau kamu mengubah menu navigasi, cukup edit header.php sekali dan semua halaman ikut berubah. Ini bentuk paling dasar dari "template" — nanti diperhalus di Modul 4.
Latihan — Buat file helpers.php berisi fungsi rupiah() dan bikinSlug(). Buat file terpisah test.php yang me-require helpers itu, lalu memakainya. Pastikan strukturnya jalan.
Materi 9 · Modul 1
Tanggal, waktu, dan string lanjutan
Mengurus tanggal dengan benar, dan mengolah teks lebih dalam.
Tanggal dan waktu
<?php
// atur zona waktu di awal — penting!
date_default_timezone_set("Asia/Jakarta");
date("Y-m-d"); // 2026-09-17
date("d/m/Y H:i"); // 17/09/2026 14:30
date("l, d F Y"); // Thursday, 17 September 2026
time(); // angka detik sejak 1970 (timestamp)Kelas DateTime — cara modern
Untuk perhitungan tanggal (selisih hari, menambah waktu), DateTime jauh lebih andal daripada mengutak-atik angka timestamp.
<?php
$sekarang = new DateTime();
$jatuhTempo = new DateTime("2026-12-31");
// format
echo $sekarang->format("d/m/Y");
// tambah / kurangi waktu
$sekarang->modify("+7 days");
$sekarang->modify("-1 month");
// selisih dua tanggal
$selisih = $sekarang->diff($jatuhTempo);
echo $selisih->days; // jumlah hari
// bandingkan tanggal langsung
if ($sekarang > $jatuhTempo) {
echo "Sudah lewat jatuh tempo";
}Hati-hati — Selalu set zona waktu dengan date_default_timezone_set("Asia/Jakarta") di awal aplikasi. Kalau tidak, server (yang biasanya diatur ke UTC) akan menampilkan waktu yang meleset 7 jam dari WIB. Ini sumber bug klasik pada jadwal, jam pesanan, dan laporan harian.
String lanjutan
<?php
// membangun string dari array
$bagian = ["produk", "kaos", "detail"];
echo implode("/", $bagian); // produk/kaos/detail
// memecah string jadi array
$csv = "Kaos,85000,24";
$kolom = explode(",", $csv); // ["Kaos", "85000", "24"]
// sprintf — membangun string dengan placeholder
$pesan = sprintf(
"Produk %s seharga Rp %s",
"Kaos",
number_format(85000, 0, ",", ".")
);
// cek awal / akhir string (PHP 8+)
str_starts_with("produk.php", "produk"); // true
str_ends_with("foto.jpg", ".jpg"); // trueRegular expression (regex) secukupnya
Regex adalah pola untuk mencocokkan teks. Rumit di awal, tapi kamu cuma butuh sedikit untuk validasi dasar.
<?php
// cocokkan pola — kembalikan 1 kalau cocok
preg_match('/^[0-9]+$/', "12345"); // 1 — semua angka
preg_match('/^08[0-9]{8,11}$/', $hp); // validasi nomor HP Indonesia
// ganti berdasarkan pola
preg_replace('/[^a-z0-9]+/', '-', $teks); // untuk membuat slugCatatan — Untuk validasi email, jangan bikin regex sendiri (rumit dan mudah salah). PHP sudah punya alat khusus: filter_var($email, FILTER_VALIDATE_EMAIL). Kita bahas ini di Modul 2 saat menangani form.
Latihan — Buat fungsi yang menerima tanggal jatuh tempo (string seperti "2026-12-31") dan mengembalikan berapa hari lagi sampai tanggal itu, atau teks "Sudah lewat" kalau sudah terlewati. Pakai DateTime.
Materi 10 · Modul 1
Error, warning, dan cara membaca pesan
PHP akan sering memberi pesan error. Memahaminya adalah keterampilan yang menghemat berjam-jam.
Jenis-jenis masalah
| Jenis | Artinya |
|---|---|
| Parse error | Salah ketik sintaks (kurang ;, kurung tidak lengkap). Program tidak jalan sama sekali. |
| Fatal error | Kesalahan serius saat berjalan (memanggil fungsi yang tidak ada). Program berhenti. |
| Warning | Ada masalah, tapi program lanjut (file tidak ditemukan di include). |
| Notice / Deprecated | Peringatan ringan (akses key array yang tidak ada, memakai fungsi lama). |
Menyalakan tampilan error saat development
<?php
// tampilkan SEMUA error — hanya saat mengembangkan, jangan di produksi
error_reporting(E_ALL);
ini_set("display_errors", "1");Hati-hati — Di produksi (server sungguhan yang diakses pengguna), display_errors harus dimatikan. Pesan error bisa membocorkan struktur folder, nama database, bahkan potongan kode — informasi berharga bagi penyerang. Di produksi, error dicatat ke file log (log_errors), bukan ditampilkan ke pengguna.
Membaca pesan error
Fatal error: Uncaught Error: Call to undefined function
hitungTota() in /var/www/produk.php:15
// cara membacanya:
// - "undefined function hitungTota" → fungsi yang dipanggil tidak ada (salah ketik?)
// - "produk.php:15" → file dan BARIS tempat masalahnya
// selalu mulai dari nama file dan nomor baris.Nomor baris hampir selalu menunjuk ke tempat masalah, atau tepat setelahnya. Untuk parse error karena kurang ; atau kurung, cek juga baris sebelum yang disebutkan.
try / catch — menangani error dengan anggun
Beberapa operasi bisa gagal karena hal di luar kendali (database mati, file tidak bisa dibaca). Daripada program crash, kamu bisa menangkap error dan menanganinya.
<?php
try {
// kode yang mungkin gagal
$hasil = 10 / $pembagi;
if ($pembagi === 0) {
throw new Exception("Tidak bisa dibagi nol");
}
} catch (Exception $e) {
// dijalankan kalau ada error di blok try
echo "Terjadi kesalahan: " . $e->getMessage();
} finally {
// selalu dijalankan, ada error atau tidak (opsional)
echo "Selesai.";
}Melempar error sendiri
<?php
function tarikSaldo(int $saldo, int $jumlah): int {
if ($jumlah > $saldo) {
throw new InvalidArgumentException("Saldo tidak cukup");
}
return $saldo - $jumlah;
}
try {
$sisa = tarikSaldo(50000, 100000);
} catch (InvalidArgumentException $e) {
echo $e->getMessage(); // Saldo tidak cukup
}Catatan — throw berarti "berhenti, ada yang salah, dan lempar masalahnya ke atas". Fungsi yang memanggil bisa menangkapnya dengan catch dan memutuskan apa yang harus dilakukan. Ini cara bersih memisahkan "mendeteksi masalah" (di dalam fungsi) dari "menangani masalah" (di pemanggil). Kita pakai pola ini banyak di Modul 4 dan 5.
Alat debugging
<?php
var_dump($data); // tipe + nilai, paling detail
print_r($data); // lebih rapi untuk array
// bungkus print_r dengan <pre> biar rapi di browser
echo "<pre>";
print_r($produk);
echo "</pre>";
// hentikan program setelah menampilkan (dump and die)
var_dump($data); die();Latihan — Buat fungsi bagiAman(int $a, int $b): float yang melempar InvalidArgumentException kalau $b nol. Panggil dari dalam try/catch dengan pembagi 0, dan tampilkan pesan errornya dengan anggun tanpa program crash.
Materi 11 · Modul 2
PHP dan web: superglobal
Cara PHP menerima data dari browser. Inilah yang membuat PHP "hidup" di web.
Superglobal: kotak masuk PHP
Saat browser mengirim permintaan, PHP menaruh semua informasinya di sejumlah array khusus yang bisa diakses dari mana saja. Namanya diawali $_.
| Superglobal | Isinya |
|---|---|
$_GET | Data dari URL (setelah tanda ?). Untuk pencarian, filter, halaman. |
$_POST | Data dari form yang dikirim dengan method POST. Untuk menyimpan, login. |
$_REQUEST | Gabungan GET + POST. Hindari — lebih baik spesifik. |
$_SERVER | Info tentang permintaan & server (URL, method, IP, browser). |
$_SESSION | Data yang bertahan antar halaman untuk satu pengguna. |
$_COOKIE | Data kecil yang disimpan di browser pengguna. |
$_FILES | File yang diunggah lewat form. |
$_GET — membaca data dari URL
<?php
// URL: produk.php?cari=kaos&urut=harga
$cari = $_GET["cari"] ?? ""; // "kaos"
$urut = $_GET["urut"] ?? "nama"; // "harga"
$page = (int)($_GET["page"] ?? 1); // paksa jadi integer
echo "Mencari: $cari, diurutkan berdasar $urut";Catatan — Selalu pakai ?? "default" saat membaca superglobal. Pengguna bisa saja membuka URL tanpa parameter itu, dan mengakses key yang tidak ada memunculkan warning. Dengan ?? kamu menyediakan nilai cadangan yang aman. Perhatikan juga (int) untuk memaksa tipe — data dari URL selalu berupa string.
$_SERVER — informasi permintaan
<?php
$_SERVER["REQUEST_METHOD"]; // "GET" atau "POST"
$_SERVER["REQUEST_URI"]; // "/produk.php?cari=kaos"
$_SERVER["REMOTE_ADDR"]; // alamat IP pengunjung
// pola penting: cek apakah ini kiriman form (POST)
if ($_SERVER["REQUEST_METHOD"] === "POST") {
// proses data form di sini
}Menyusun URL dengan query string
<?php
// http_build_query membangun query string dengan aman
$params = ["cari" => "kaos polos", "page" => 2];
$url = "produk.php?" . http_build_query($params);
// produk.php?cari=kaos+polos&page=2 (spasi otomatis di-encode)Selalu pakai http_build_query daripada menyambung string sendiri — dia otomatis meng-encode karakter khusus (spasi, &, dll) supaya URL-nya valid.
Latihan — Buat halaman salam.php yang membaca ?nama=... dari URL dan menampilkan "Halo, [nama]!". Kalau parameter tidak ada, tampilkan "Halo, Tamu!". Uji dengan beberapa URL berbeda.
Materi 12 · Modul 2
Menangani form
Menerima, memvalidasi, dan memproses data yang dikirim pengguna.
Form HTML dan method
<form action="simpan.php" method="POST">
<input type="text" name="nama">
<input type="number" name="harga">
<button type="submit">Simpan</button>
</form>| Method | Kapan dipakai |
|---|---|
| GET | Membaca / mencari data. Data tampil di URL. Aman diulang & di-bookmark. |
| POST | Mengubah data (simpan, hapus, login). Data tidak tampil di URL. |
Catatan — Aturan sederhana: kalau aksinya mengubah sesuatu (menyimpan pesanan, menghapus produk, login), pakai POST. Kalau cuma membaca (mencari, memfilter, buka halaman), pakai GET. Atribut name pada input menentukan kunci di $_POST — name="nama" jadi $_POST["nama"].
Pola satu halaman: form + proses
Pola yang sangat umum: satu file menampilkan form dan memprosesnya. Kalau method POST, proses dulu; kalau tidak, tampilkan form.
<?php
declare(strict_types=1);
$errors = [];
$sukses = false;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
// 1. ambil dan bersihkan input
$nama = trim($_POST["nama"] ?? "");
$harga = (int)($_POST["harga"] ?? 0);
// 2. validasi
if ($nama === "") {
$errors["nama"] = "Nama wajib diisi";
}
if ($harga <= 0) {
$errors["harga"] = "Harga harus lebih dari 0";
}
// 3. kalau tidak ada error, simpan (nanti ke database)
if (empty($errors)) {
// ... simpan data ...
$sukses = true;
}
}
?>
<?php if ($sukses): ?>
<p>✅ Produk berhasil disimpan!</p>
<?php endif; ?>
<form method="POST">
<input type="text" name="nama" value="<?= htmlspecialchars($_POST['nama'] ?? '') ?>">
<?php if (isset($errors["nama"])): ?>
<span style="color:red"><?= $errors["nama"] ?></span>
<?php endif; ?>
<input type="number" name="harga">
<?php if (isset($errors["harga"])): ?>
<span style="color:red"><?= $errors["harga"] ?></span>
<?php endif; ?>
<button>Simpan</button>
</form>Catatan — Perhatikan value="<?= htmlspecialchars($_POST['nama'] ?? '') ?>". Ini mengisi ulang form dengan data yang tadi dimasukkan pengguna kalau validasi gagal — supaya mereka tidak perlu mengetik ulang semuanya. htmlspecialchars() di sini penting untuk keamanan; dibahas di Materi 15.
Validasi bawaan dengan filter_var
<?php
// email
$email = filter_var($_POST["email"], FILTER_VALIDATE_EMAIL);
if ($email === false) {
$errors["email"] = "Format email tidak valid";
}
// angka dalam rentang
$umur = filter_var($_POST["umur"], FILTER_VALIDATE_INT, [
"options" => ["min_range" => 1, "max_range" => 120],
]);
// URL
filter_var($web, FILTER_VALIDATE_URL);Pola PRG: cegah kirim ulang
Setelah memproses POST berhasil, alihkan (redirect) ke halaman lain. Ini mencegah masalah "form terkirim dua kali" saat pengguna menekan refresh.
<?php
if ($_SERVER["REQUEST_METHOD"] === "POST" && empty($errors)) {
// ... simpan data ...
// alihkan ke halaman daftar. header() HARUS sebelum ada output apa pun.
header("Location: produk.php?sukses=1");
exit; // WAJIB — hentikan skrip setelah redirect
}Hati-hati — header() harus dipanggil sebelum ada output apa pun (bahkan satu spasi atau baris kosong sebelum <?php). Kalau tidak, muncul error "headers already sent". Dan selalu ikuti dengan exit; — tanpa itu, kode di bawahnya tetap jalan meski browser sudah diarahkan pergi.
Latihan — Buat form kontak (nama, email, pesan) dalam satu file. Validasi: nama wajib, email harus valid, pesan minimal 10 karakter. Tampilkan error per field, isi ulang form kalau gagal, dan tampilkan pesan sukses kalau semua valid.
Materi 13 · Modul 2
Session dan cookie
Cara PHP "mengingat" pengguna antar halaman — untuk login, keranjang belanja, dan preferensi.
Kenapa perlu session
Ingat dari Materi 1: setiap permintaan mulai dari nol. Jadi bagaimana website tahu kamu sudah login saat pindah halaman? Jawabannya: session. PHP menyimpan data di server dan menandai browser-mu dengan sebuah ID, jadi tiap permintaan dari browser yang sama bisa mengambil data yang tersimpan.
Menggunakan session
<?php
session_start(); // WAJIB di baris paling atas, sebelum output apa pun
// menyimpan data
$_SESSION["user_id"] = 42;
$_SESSION["nama"] = "Nus";
// membaca data (di halaman mana pun, setelah session_start)
$id = $_SESSION["user_id"] ?? null;
// cek apakah pengguna sudah login
if (isset($_SESSION["user_id"])) {
echo "Selamat datang kembali!";
}
// menghapus satu data
unset($_SESSION["nama"]);
// logout — hapus semua session
session_destroy();Hati-hati — session_start() harus dipanggil sebelum output apa pun (sama seperti header()). Letakkan di baris paling atas setiap halaman yang butuh session. Kalau lupa, $_SESSION tidak akan berfungsi.
Contoh: proteksi halaman
Pola yang sangat umum: halaman yang cuma boleh dibuka pengguna yang sudah login.
<?php
session_start();
// kalau belum login, tendang ke halaman login
if (!isset($_SESSION["user_id"])) {
header("Location: login.php");
exit;
}
// kode di bawah ini hanya jalan untuk pengguna yang login
echo "Halaman rahasia untuk user #" . $_SESSION["user_id"];Flash message: pesan sekali tampil
<?php
session_start();
// di halaman yang memproses (sebelum redirect)
$_SESSION["flash"] = "Produk berhasil disimpan!";
header("Location: produk.php");
exit;
// di halaman tujuan: tampilkan lalu hapus
if (isset($_SESSION["flash"])) {
echo "<div class='alert'>" . $_SESSION["flash"] . "</div>";
unset($_SESSION["flash"]); // hapus supaya tak muncul lagi saat refresh
}Cookie
Berbeda dari session (disimpan di server), cookie disimpan di browser pengguna. Cocok untuk preferensi ringan yang tidak rahasia, seperti pilihan tema.
<?php
// set cookie — nama, nilai, kapan kedaluwarsa
setcookie("tema", "gelap", time() + (86400 * 30)); // 30 hari
// baca cookie (baru tersedia di permintaan BERIKUTNYA)
$tema = $_COOKIE["tema"] ?? "terang";
// hapus cookie — set waktu kedaluwarsa ke masa lalu
setcookie("tema", "", time() - 3600);Hati-hati — Jangan pernah menyimpan data sensitif (password, saldo, hak akses) di cookie. Cookie ada di komputer pengguna dan bisa mereka ubah. Untuk apa pun yang menyangkut keamanan atau identitas, pakai session (yang datanya di server); cookie cuma menyimpan ID sesi yang acak.
Latihan — Buat sistem login sangat sederhana: login.php dengan form (username/password di-cek dengan nilai tetap dulu), yang menyimpan $_SESSION["user"] saat benar dan redirect ke dashboard.php. Buat dashboard.php yang menolak akses kalau belum login, dan logout.php yang menghapus session.
Materi 14 · Modul 2
Upload file
Menerima file dari pengguna dengan aman — foto produk, dokumen, dan sebagainya.
Form upload
Form untuk upload butuh dua hal khusus: method POST dan atribut enctype.
<form method="POST" enctype="multipart/form-data">
<input type="file" name="foto" accept="image/*">
<button>Unggah</button>
</form>Hati-hati — Tanpa enctype="multipart/form-data", file tidak akan terkirim dan $_FILES akan kosong. Ini kesalahan yang paling sering terjadi saat pertama membuat fitur upload.
Memproses upload dengan aman
<?php
declare(strict_types=1);
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$file = $_FILES["foto"] ?? null;
// 1. pastikan upload berhasil
if (!$file || $file["error"] !== UPLOAD_ERR_OK) {
die("Upload gagal");
}
// 2. batasi ukuran (misal maks 2 MB)
if ($file["size"] > 2 * 1024 * 1024) {
die("File terlalu besar (maks 2MB)");
}
// 3. cek tipe SEBENARNYA, bukan dari nama file (ekstensi bisa dipalsukan)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file["tmp_name"]);
$bolehkan = ["image/jpeg", "image/png", "image/webp"];
if (!in_array($mime, $bolehkan, true)) {
die("Tipe file tidak diizinkan");
}
// 4. buat nama file acak — JANGAN pakai nama asli dari pengguna
$ext = match($mime) {
"image/jpeg" => "jpg",
"image/png" => "png",
"image/webp" => "webp",
};
$namaBaru = bin2hex(random_bytes(16)) . "." . $ext;
$tujuan = "uploads/" . $namaBaru;
// 5. pindahkan dari lokasi sementara ke folder tujuan
if (move_uploaded_file($file["tmp_name"], $tujuan)) {
echo "Tersimpan sebagai $namaBaru";
}
}Hati-hati — Tiga aturan wajib untuk keamanan upload: (1) Cek tipe file dari isinya (finfo), bukan dari ekstensi nama — penyerang bisa menamai file berbahaya foto.jpg. (2) Beri nama baru yang acak, jangan pakai nama asli dari pengguna. (3) Simpan file di folder yang tidak bisa dieksekusi sebagai skrip. Upload adalah salah satu titik masuk serangan paling umum.
Kode error upload
| Konstanta | Artinya |
|---|---|
UPLOAD_ERR_OK | Berhasil. |
UPLOAD_ERR_INI_SIZE | Lebih besar dari batas server (upload_max_filesize). |
UPLOAD_ERR_NO_FILE | Tidak ada file yang dipilih. |
UPLOAD_ERR_PARTIAL | Upload terputus di tengah jalan. |
Latihan — Buat form upload foto yang: menerima hanya JPG/PNG, menolak file di atas 1 MB, menyimpan dengan nama acak di folder uploads/, lalu menampilkan gambar yang baru diunggah. Uji juga dengan file yang salah tipe untuk memastikan penolakannya bekerja.
Materi 15 · Modul 2
Keamanan dasar web
Tiga celah paling umum, dan cara menutupnya. Wajib dipahami sebelum bikin aplikasi sungguhan.
1. XSS — menampilkan input pengguna dengan aman
Kalau kamu menampilkan teks dari pengguna langsung ke HTML, penyerang bisa menyisipkan kode. Bayangkan seseorang mengisi nama dengan <script>curi_data()</script> — kalau ditampilkan mentah, skrip itu jalan di browser pengunjung lain.
<?php
// ❌ BERBAHAYA — menampilkan input mentah
echo "Halo, " . $_GET["nama"];
// ✅ AMAN — ubah karakter HTML jadi teks biasa dulu
echo "Halo, " . htmlspecialchars($_GET["nama"]);
// <script> jadi <script> — tampil sebagai teks, tidak dieksekusiAturan emas — Setiap kali menampilkan data yang berasal dari pengguna (nama, komentar, apa pun dari $_GET/$_POST/database), bungkus dengan htmlspecialchars(). Buat kebiasaan ini otomatis. Banyak orang membuat fungsi pendek seperti function e($t) { return htmlspecialchars($t ?? '', ENT_QUOTES); } lalu memakai <?= e($nama) ?> di mana-mana.
2. SQL injection — pengantar
Saat kamu menyusun query database dengan menyambung input pengguna langsung, penyerang bisa menyuntikkan perintah database. Ini bahaya paling serius, dan dibahas tuntas di handbook MySQL. Prinsipnya:
<?php
// ❌ BERBAHAYA — jangan pernah menyambung input ke query
$sql = "SELECT * FROM produk WHERE nama = '" . $_GET["cari"] . "'";
// ✅ AMAN — pakai prepared statement (dibahas di handbook MySQL)
$stmt = $pdo->prepare("SELECT * FROM produk WHERE nama = ?");
$stmt->execute([$_GET["cari"]]);Hati-hati — Aturan mutlak: jangan pernah memasukkan input pengguna langsung ke dalam string query. Selalu pakai prepared statement dengan placeholder (?). Ini bukan opsional — satu celah SQL injection bisa membocorkan atau menghapus seluruh database. Handbook MySQL membahas caranya secara lengkap.
3. CSRF — melindungi form yang mengubah data
Penyerang bisa menipu pengguna yang sedang login agar tanpa sadar mengirim form ke situsmu (misalnya menghapus akun) dari situs lain. Perlindungannya: token rahasia yang hanya ada di form aslimu.
<?php
session_start();
// saat menampilkan form: buat token dan simpan di session
if (empty($_SESSION["csrf"])) {
$_SESSION["csrf"] = bin2hex(random_bytes(32));
}
?>
<form method="POST">
<input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
<!-- ... field lain ... -->
</form>
<?php
// saat memproses POST: pastikan token cocok
if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (!hash_equals($_SESSION["csrf"] ?? "", $_POST["csrf"] ?? "")) {
die("Token tidak valid");
}
// ... aman, proses data ...
}Menyimpan password dengan benar
<?php
// saat mendaftar: acak password (hash), JANGAN simpan apa adanya
$hash = password_hash($password, PASSWORD_DEFAULT);
// simpan $hash ke database
// saat login: cocokkan password dengan hash tersimpan
if (password_verify($passwordInput, $hashTersimpan)) {
echo "Login berhasil";
}Hati-hati — Jangan pernah menyimpan password apa adanya di database, dan jangan mengacaknya dengan MD5/SHA1 (sudah tidak aman). Selalu pakai password_hash() dan password_verify(). Keduanya bawaan PHP, otomatis memakai algoritma kuat dan menambahkan pengaman (salt) sendiri. Kamu tidak perlu paham detail kriptografinya — cukup pakai dua fungsi ini.
Latihan — Ambil form kontak dari latihan Materi 12. Tambahkan: (1) htmlspecialchars saat menampilkan ulang input, (2) token CSRF yang diverifikasi saat submit. Uji bahwa form menolak submit tanpa token yang benar.
Materi 16 · Modul 3
Class dan object
Cara mengelompokkan data dan fungsi yang berkaitan jadi satu kesatuan. Dasar dari kode berskala besar.
Kenapa OOP
Sejauh ini kamu menyimpan data di array dan mengolahnya dengan fungsi terpisah. Itu bekerja, tapi saat aplikasi tumbuh, data dan fungsi yang berkaitan tersebar ke mana-mana. OOP (Object-Oriented Programming) menyatukannya: sebuah class adalah cetakan yang menggabungkan data (properti) dan perilaku (method) dari satu jenis benda.
Class dan object pertama
<?php
declare(strict_types=1);
// class = cetakan / blueprint
class Produk {
// properti = data yang dimiliki tiap produk
public string $nama;
public int $harga;
public int $stok = 0; // boleh punya nilai default
// method = fungsi milik class
public function hargaFormat(): string {
// $this = object yang sedang dipakai
return "Rp " . number_format($this->harga, 0, ",", ".");
}
public function tersedia(): bool {
return $this->stok > 0;
}
}
// object = wujud nyata dari cetakan (instance)
$kaos = new Produk();
$kaos->nama = "Kaos Polos";
$kaos->harga = 85000;
$kaos->stok = 12;
echo $kaos->nama; // Kaos Polos
echo $kaos->hargaFormat(); // Rp 85.000
var_dump($kaos->tersedia()); // bool(true)Catatan — Dua istilah kunci: class adalah cetakannya (definisi Produk), object adalah barang jadinya ($kaos). Dari satu class kamu bisa membuat banyak object. $this di dalam method menunjuk ke object yang sedang bekerja saat itu. Panah -> dipakai untuk mengakses properti dan method sebuah object.
Constructor: mengisi saat dibuat
Daripada mengisi properti satu per satu, constructor menjalankan kode otomatis saat object dibuat.
<?php
declare(strict_types=1);
class Produk {
// constructor property promotion (PHP 8+):
// deklarasi properti + isi otomatis, sekaligus, di parameter
public function __construct(
public string $nama,
public int $harga,
public int $stok = 0,
) {}
public function hargaFormat(): string {
return "Rp " . number_format($this->harga, 0, ",", ".");
}
}
// sekarang isi langsung saat membuat — jauh lebih ringkas
$kaos = new Produk("Kaos Polos", 85000, 12);
$topi = new Produk(nama: "Topi", harga: 45000); // stok default 0
echo $kaos->hargaFormat(); // Rp 85.000Catatan — Constructor property promotion (menulis public langsung di parameter constructor) adalah fitur PHP 8 yang menghapus banyak kode berulang. Tanpa itu kamu harus mendeklarasikan properti di atas, lalu menulis $this->nama = $nama; untuk tiap properti. Dengan promotion, cukup satu baris.
Object di dalam array
Pola umum: daftar object, lalu diproses dengan foreach — persis seperti data dari database nanti.
<?php
$katalog = [
new Produk("Kaos", 85000, 12),
new Produk("Hoodie", 210000, 0),
new Produk("Topi", 45000, 5),
];
foreach ($katalog as $p) {
$badge = $p->tersedia() ? "✅" : "❌ habis";
echo "$p->nama — $p->hargaFormat() $badge\n";
}Latihan — Buat class Pelanggan dengan properti nama, email, dan poin (default 0). Tambahkan method tambahPoin(int $jumlah) dan level(): string yang mengembalikan "Perak", "Emas", atau "Platinum" berdasarkan poin. Buat beberapa object dan uji.
Materi 17 · Modul 3
Enkapsulasi: public, private, protected
Mengontrol bagian mana dari sebuah object yang boleh diakses dari luar. Kunci kode yang aman dari kesalahan.
Tiga tingkat akses
| Modifier | Bisa diakses dari |
|---|---|
public | Mana saja — dari luar maupun dalam class. |
private | Hanya dari dalam class itu sendiri. |
protected | Dari class itu dan class turunannya (dibahas di Materi 18). |
Kenapa menyembunyikan itu penting
Bayangkan saldo rekening sebagai properti public. Siapa pun bisa menulis $akun->saldo = -999999; — merusak aturan bisnis. Dengan private, saldo hanya bisa diubah lewat method yang kamu kontrol, yang bisa menegakkan aturan.
<?php
declare(strict_types=1);
class Rekening {
// private: tidak bisa disentuh langsung dari luar
private int $saldo = 0;
public function setor(int $jumlah): void {
if ($jumlah <= 0) {
throw new InvalidArgumentException("Jumlah harus positif");
}
$this->saldo += $jumlah;
}
public function tarik(int $jumlah): void {
if ($jumlah > $this->saldo) {
throw new RuntimeException("Saldo tidak cukup");
}
$this->saldo -= $jumlah;
}
// cara aman untuk MEMBACA saldo — hanya baca, tak bisa diubah
public function saldo(): int {
return $this->saldo;
}
}
$akun = new Rekening();
$akun->setor(100000);
$akun->tarik(30000);
echo $akun->saldo(); // 70000
// $akun->saldo = 999; ← ERROR: tidak bisa akses properti private
$akun->tarik(999999); // melempar RuntimeException: saldo tidak cukupCatatan — Ini prinsip enkapsulasi: sembunyikan data, sediakan pintu terkontrol untuk mengaksesnya. Object jadi menjaga aturannya sendiri — mustahil masuk ke keadaan tidak valid. : void pada method berarti "tidak mengembalikan nilai apa pun". Aturan praktis: buat properti private secara default, buka jadi public hanya kalau memang perlu.
Getter, setter, dan validasi
<?php
class Produk {
private int $harga;
public function setHarga(int $harga): void {
if ($harga < 0) {
throw new InvalidArgumentException("Harga tak boleh negatif");
}
$this->harga = $harga;
}
public function getHarga(): int {
return $this->harga;
}
}Dengan setter, setiap perubahan harga otomatis lewat pengecekan — mustahil menyetel harga negatif dari mana pun di aplikasi.
Latihan — Ubah class Rekening: tambahkan riwayat transaksi (array private) yang mencatat tiap setor/tarik. Tambahkan method riwayat(): array untuk membacanya. Pastikan riwayat tidak bisa diubah dari luar kecuali lewat setor/tarik.
Materi 18 · Modul 3
Inheritance dan abstraksi
Membuat class baru berdasarkan class yang sudah ada, tanpa menulis ulang kode yang sama.
Inheritance: mewarisi dari induk
<?php
declare(strict_types=1);
// class induk (parent)
class Pegawai {
public function __construct(
protected string $nama,
protected int $gajiPokok,
) {}
public function totalGaji(): int {
return $this->gajiPokok;
}
}
// class anak — mewarisi semua dari Pegawai dengan "extends"
class Manajer extends Pegawai {
public function __construct(
string $nama,
int $gajiPokok,
private int $tunjangan,
) {
// panggil constructor induk
parent::__construct($nama, $gajiPokok);
}
// override — ganti method induk dengan versi sendiri
public function totalGaji(): int {
return $this->gajiPokok + $this->tunjangan;
}
}
$staf = new Pegawai("Rifki", 5000000);
$manajer = new Manajer("Nus", 8000000, 3000000);
echo $staf->totalGaji(); // 5000000
echo $manajer->totalGaji(); // 11000000 (pakai versi override)Catatan — extends berarti "adalah jenis dari". Manajer adalah jenis Pegawai, jadi mewarisi semua properti dan method-nya, lalu boleh menambah atau mengganti. protected (bukan private) pada $nama penting di sini — supaya class anak juga bisa mengaksesnya. parent:: memanggil versi milik induk.
Abstract class: cetakan yang wajib dilengkapi
Kadang kamu ingin mendefinisikan bentuk umum tapi memaksa tiap turunan mengisi detailnya. abstract class tidak bisa dibuat objectnya langsung — hanya sebagai dasar.
<?php
abstract class MetodeBayar {
// method biasa — diwarisi apa adanya
public function konfirmasi(): string {
return "Pembayaran via " . $this->nama();
}
// abstract method — WAJIB diisi tiap turunan
abstract public function nama(): string;
abstract public function biayaAdmin(int $jumlah): int;
}
class Transfer extends MetodeBayar {
public function nama(): string { return "Transfer Bank"; }
public function biayaAdmin(int $jumlah): int { return 0; }
}
class QRIS extends MetodeBayar {
public function nama(): string { return "QRIS"; }
public function biayaAdmin(int $jumlah): int {
return (int) round($jumlah * 0.007); // 0.7%
}
}
// $x = new MetodeBayar(); ← ERROR: abstract tak bisa di-instantiate
$bayar = new QRIS();
echo $bayar->konfirmasi(); // Pembayaran via QRIS
echo $bayar->biayaAdmin(100000); // 700Hati-hati — Jangan berlebihan dengan inheritance. Rantai warisan yang dalam (anak dari anak dari anak) membuat kode sulit dilacak. Aturan praktis: pakai inheritance hanya untuk hubungan "adalah jenis dari" yang jelas. Untuk berbagi kemampuan tanpa hubungan induk-anak, ada interface (materi berikutnya) yang seringkali lebih baik.
Latihan — Buat abstract class Bangun dengan abstract method luas(): float dan method biasa deskripsi(). Buat turunan Persegi dan Lingkaran yang masing-masing menghitung luasnya. Simpan beberapa dalam array dan tampilkan luas semuanya dengan foreach.
Materi 19 · Modul 3
Interface, trait, dan enum
Tiga alat OOP untuk menyusun kode yang fleksibel dan bisa ditukar-tukar.
Interface: kontrak kemampuan
Interface mendefinisikan method apa yang harus dimiliki sebuah class, tanpa mengatur bagaimana isinya. Kode lain cukup tahu class itu memenuhi kontrak, tanpa peduli detailnya.
<?php
declare(strict_types=1);
// kontrak: apa pun yang "bisa dikirim notifikasi" harus punya method kirim()
interface Notifikasi {
public function kirim(string $pesan): bool;
}
// implements = "menandatangani kontrak ini"
class EmailNotif implements Notifikasi {
public function kirim(string $pesan): bool {
// ... kirim email ...
return true;
}
}
class WhatsAppNotif implements Notifikasi {
public function kirim(string $pesan): bool {
// ... kirim WhatsApp ...
return true;
}
}
// fungsi ini menerima APA PUN yang memenuhi kontrak Notifikasi
function beritahu(Notifikasi $channel, string $pesan): void {
$channel->kirim($pesan);
}
beritahu(new EmailNotif(), "Pesanan diterima");
beritahu(new WhatsAppNotif(), "Pesanan diterima");
// keduanya jalan — beritahu() tak peduli caranya, asal memenuhi kontrakCatatan — Inilah kekuatan interface: kode-mu bergantung pada kontrak, bukan implementasi tertentu. Kamu bisa menukar EmailNotif dengan WhatsAppNotif tanpa mengubah fungsi beritahu(). Ini membuat kode gampang diperluas dan diuji. Sebuah class boleh implements banyak interface sekaligus.
Trait: berbagi kode antar class
Trait adalah potongan method yang bisa "ditempel" ke banyak class yang tidak berhubungan. Berguna untuk kemampuan umum yang dipakai di mana-mana.
<?php
trait PunyaTimestamp {
public ?string $dibuatPada = null;
public function catatWaktu(): void {
$this->dibuatPada = date("Y-m-d H:i:s");
}
}
// tempel trait ke class mana pun yang butuh
class Produk {
use PunyaTimestamp;
// ... properti produk ...
}
class Pesanan {
use PunyaTimestamp;
// ... properti pesanan ...
}
$p = new Produk();
$p->catatWaktu();
echo $p->dibuatPada; // 2026-09-17 14:30:00Enum: kumpulan nilai tetap
Enum (PHP 8.1+) mendefinisikan sekumpulan pilihan yang sah. Menggantikan "string ajaib" yang rawan salah ketik.
<?php
// tanpa enum: "lunas", "pending"... rawan typo, tak ada yang mengecek
// dengan enum: pilihan pasti, editor bisa autocomplete
enum StatusPesanan: string {
case Pending = "pending";
case Lunas = "lunas";
case Dikirim = "dikirim";
case Batal = "batal";
// enum boleh punya method
public function label(): string {
return match($this) {
StatusPesanan::Pending => "Menunggu Pembayaran",
StatusPesanan::Lunas => "Sudah Dibayar",
StatusPesanan::Dikirim => "Dalam Pengiriman",
StatusPesanan::Batal => "Dibatalkan",
};
}
}
$status = StatusPesanan::Lunas;
echo $status->value; // "lunas" (untuk disimpan ke database)
echo $status->label(); // "Sudah Dibayar" (untuk ditampilkan)
// ubah dari string (misal dari database) jadi enum
$dari = StatusPesanan::from("dikirim"); // StatusPesanan::DikirimCatatan — Enum menghilangkan seluruh kelas bug "salah ketik status". Kalau kamu menulis StatusPesanan::Lunaz, editor langsung menandai error — beda dengan string "lunaz" yang baru ketahuan saat aplikasi berjalan. Pakai enum untuk apa pun yang punya pilihan tetap: status, peran pengguna, metode bayar.
Latihan — Buat enum Peran (Admin, Kasir, Pemilik) dengan method bisaHapus(): bool yang hanya true untuk Admin dan Pemilik. Buat interface Laporan dengan method generate(): string, lalu dua class yang meng-implement-nya dengan cara berbeda.
Materi 20 · Modul 3
Static dan konstanta class
Anggota class yang dimiliki bersama, bukan per object. Untuk pembantu dan nilai tetap.
Property dan method static
Anggota static dimiliki oleh class, bukan tiap object. Diakses dengan :: tanpa perlu membuat object.
<?php
declare(strict_types=1);
class Uang {
// konstanta class — nilai tetap milik class
const MATA_UANG = "IDR";
// method static — helper yang tak butuh object
public static function format(int $angka): string {
return "Rp " . number_format($angka, 0, ",", ".");
}
}
// panggil langsung lewat class, tanpa "new"
echo Uang::format(85000); // Rp 85.000
echo Uang::MATA_UANG; // IDRCatatan — Method static cocok untuk fungsi pembantu murni yang tidak butuh data object — seperti pemformat, konverter, pembuat ID. Bedanya dengan fungsi biasa: static mengelompokkan pembantu terkait di bawah satu nama class yang rapi (Uang::format, Uang::konversi).
Counter bersama
<?php
class Pesanan {
// static property — DIBAGI oleh semua object
public static int $jumlahDibuat = 0;
public function __construct() {
// self:: menunjuk ke class ini sendiri
self::$jumlahDibuat++;
}
}
new Pesanan();
new Pesanan();
new Pesanan();
echo Pesanan::$jumlahDibuat; // 3 — dihitung lintas semua objectStatic factory: cara rapi membuat object
Method static sering dipakai sebagai "pabrik" untuk membuat object dengan cara yang lebih jelas namanya.
<?php
class Produk {
public function __construct(
public string $nama,
public int $harga,
) {}
// buat object dari array (misal satu baris data dari database)
public static function dariArray(array $data): static {
return new static(
nama: $data["nama"],
harga: (int) $data["harga"],
);
}
}
// baris data dari database → object, dalam satu langkah rapi
$baris = ["nama" => "Kaos", "harga" => "85000"];
$produk = Produk::dariArray($baris);Hati-hati — Jangan menjadikan segalanya static. Method static tidak bisa mengakses data object ($this) dan sulit ditukar saat pengujian. Pakai static hanya untuk pembantu murni (yang keluarannya cuma bergantung pada inputnya) dan konstanta. Untuk logika yang mengelola data, gunakan object biasa.
Menyelesaikan Modul 3
Kamu sekarang punya semua alat OOP inti: class dan object, enkapsulasi, inheritance, abstraksi, interface, trait, enum, dan static. Bersama-sama, inilah yang membuat kode besar tetap teratur. Framework PHP modern dibangun sepenuhnya di atas konsep-konsep ini — begitu kamu nyaman dengannya, membaca kode framework jadi jauh lebih mudah.
Latihan — Gabungkan semuanya: buat interface Kendaraan (method info(): string), abstract class KendaraanDasar yang meng-implement-nya sebagian, lalu class konkret Motor dan Mobil. Tambahkan enum JenisBahanBakar dan static counter jumlah kendaraan yang dibuat. Uji semuanya.
Materi 21 · Modul 4
Composer: package manager PHP
Alat untuk memakai kode orang lain, dan mengatur kode sendiri secara profesional.
Kenapa butuh Composer
Sejauh ini kita menulis semua fungsi sendiri dan memakai require manual. Untuk hal-hal umum (mengirim email, membaca file Excel, memvalidasi data kompleks), biasanya lebih baik memakai library yang sudah teruji banyak orang. Composer adalah alat untuk mengunduh, mengatur versi, dan memuat library-library itu secara otomatis.
Instalasi dan memulai project
# unduh dari getcomposer.org, lalu cek terpasang
composer --version
# mulai project baru — akan membuat composer.json
composer init
# pasang sebuah library, contoh: untuk generate PDF
composer require dompdf/dompdf
# pasang library HANYA untuk development (misal testing)
composer require --dev phpunit/phpunitcomposer.json: identitas project
{
"name": "onlenkan/toko-app",
"require": {
"php": ">=8.1",
"dompdf/dompdf": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}File ini mencatat: nama project, PHP minimal, semua library yang dipakai beserta versinya, dan aturan autoload (dibahas materi berikutnya). Setiap kali kamu composer require, baris baru otomatis ditambahkan ke sini.
Memakai library yang terpasang
<?php
// satu baris ini memuat SEMUA library yang terpasang
require "vendor/autoload.php";
use Dompdf\Dompdf;
$pdf = new Dompdf();
$pdf->loadHtml("<h1>Halo</h1>");
$pdf->render();Catatan — Folder vendor/ berisi semua kode library yang diunduh Composer. Folder ini tidak ikut disimpan di Git (ditambahkan ke .gitignore) karena bisa dibuat ulang kapan saja dengan composer install. Yang disimpan cukup composer.json dan composer.lock (mencatat versi persis yang terpasang, supaya semua orang di tim memakai versi yang sama).
Mencari library
Situs packagist.org adalah direktori resmi paket Composer. Sebelum menulis sesuatu yang umum dari nol (kirim email, baca Excel, validasi kompleks), cek dulu di sana — kemungkinan besar sudah ada library matang untuk itu.
Latihan — Buat project baru dengan composer init. Pasang nesbot/carbon (library tanggal yang populer). Buat file yang memakainya untuk menampilkan "3 hari lagi" dari sebuah tanggal, menggantikan perhitungan manual dengan DateTime yang kamu pakai di Materi 9.
Materi 22 · Modul 4
Namespace dan autoloading
Cara mengorganisir banyak class tanpa tabrakan nama, dan memuatnya otomatis tanpa require manual.
Masalah yang diselesaikan namespace
Bayangkan project besar dengan puluhan class Produk, User, Order — dan sebuah library yang kamu pasang juga punya class bernama User. Tanpa namespace, PHP bingung yang mana yang dimaksud. Namespace adalah cara mengelompokkan class ke dalam "folder virtual" supaya nama yang sama bisa hidup berdampingan.
Mendeklarasikan dan memakai namespace
<?php
declare(strict_types=1);
namespace App\Models;
class Produk {
public function __construct(
public string $nama,
public int $harga,
) {}
}<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Produk; // "impor" class dari namespace lain
class LaporanService {
public function ringkas(Produk $p): string {
return "Laporan untuk $p->nama";
}
}Konvensinya: namespace mengikuti struktur folder. App\Models\Produk berarti class Produk ada di src/Models/Produk.php (dengan aturan yang kita atur sebentar lagi lewat Composer).
Autoload PSR-4: tanpa require manual
Ingat "autoload" di composer.json pada materi sebelumnya? Itu memberitahu Composer: "kalau ada yang butuh class di namespace App\..., cari filenya di folder src/..., secara otomatis."
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}# setelah mengubah bagian autoload, generate ulang
composer dump-autoload<?php
require "vendor/autoload.php"; // satu baris ini menggantikan SEMUA require manual
use App\Models\Produk;
use App\Services\LaporanService;
$produk = new Produk("Kaos", 85000);
$layanan = new LaporanService();
echo $layanan->ringkas($produk);Catatan — Ini yang membuat require_once manual (Materi 8) jarang dibutuhkan lagi di project modern. Selama nama file dan namespace-nya konsisten mengikuti PSR-4 (nama class = nama file, struktur namespace = struktur folder), PHP otomatis menemukan file yang tepat begitu class-nya dipakai — tanpa kamu menulis satu pun require untuk class sendiri.
Struktur project yang dihasilkan
toko-app/
├── composer.json
├── vendor/ ← dibuat Composer, jangan diubah manual
├── src/
│ ├── Models/
│ │ └── Produk.php ← namespace App\Models
│ └── Services/
│ └── LaporanService.php ← namespace App\Services
└── index.phpLatihan — Pindahkan class Rekening dan Produk dari latihan-latihan sebelumnya ke struktur src/Models/ dengan namespace App\Models. Atur autoload PSR-4 di composer.json, jalankan composer dump-autoload, lalu pakai keduanya dari index.php tanpa satu pun require manual untuk file sendiri.
Materi 23 · Modul 4
Exception lanjutan
Membuat jenis error sendiri, dan menangani banyak jenis error secara berbeda.
Custom exception
PHP menyediakan class Exception bawaan, tapi kamu bisa membuat jenis error sendiri yang lebih spesifik — bikin kode lebih jelas maksudnya, dan bisa ditangkap secara berbeda dari error lain.
<?php
declare(strict_types=1);
namespace App\Exceptions;
// extends Exception — mewarisi semua kemampuan Exception biasa
class StokTidakCukupException extends \Exception {
public function __construct(
public readonly string $namaProduk,
public readonly int $diminta,
public readonly int $tersedia,
) {
parent::__construct(
"Stok $namaProduk tidak cukup: diminta $diminta, tersedia $tersedia"
);
}
}<?php
use App\Exceptions\StokTidakCukupException;
function kurangiStok(string $nama, int $stok, int $diminta): int {
if ($diminta > $stok) {
throw new StokTidakCukupException($nama, $diminta, $stok);
}
return $stok - $diminta;
}
try {
kurangiStok("Kaos", 5, 10);
} catch (StokTidakCukupException $e) {
// bisa akses properti khusus, bukan cuma pesan teks
echo "Kurang " . ($e->diminta - $e->tersedia) . " unit";
}Catatan — readonly (PHP 8.1+) berarti properti hanya bisa diisi sekali, saat object dibuat — cocok untuk data error yang tidak boleh berubah setelah dilempar. Custom exception seperti ini membawa data tentang error (bukan cuma teks), yang bisa dipakai kode penangkapnya untuk merespons dengan tepat.
Menangkap berurutan dari yang spesifik
Kamu bisa menangkap beberapa jenis exception berbeda dari satu blok try. Urutkan dari yang paling spesifik ke paling umum.
<?php
try {
prosesPesanan($data);
} catch (StokTidakCukupException $e) {
echo "Stok kurang: " . $e->getMessage();
} catch (\InvalidArgumentException $e) {
echo "Data tidak valid: " . $e->getMessage();
} catch (\Exception $e) {
// jaring pengaman — menangkap semua Exception lain yang tak terduga
echo "Terjadi kesalahan tak terduga";
error_log($e->getMessage()); // catat ke log, jangan tampilkan detail ke pengguna
}
// PHP 8+: gabung beberapa tipe dalam satu catch dengan |
try {
// ...
} catch (\TypeError | \ValueError $e) {
echo "Input bermasalah";
}Hati-hati — Jangan pernah menampilkan $e->getMessage() mentah-mentah ke pengguna di produksi untuk error yang tak terduga — bisa membocorkan detail teknis. Pola yang aman: catat detail lengkap ke log dengan error_log(), tampilkan pesan generik ke pengguna ("Terjadi kesalahan, coba lagi"). Untuk error yang memang ditujukan bagi pengguna (seperti stok kurang), pesannya boleh spesifik.
Latihan — Buat SaldoTidakCukupException dengan properti saldoSaatIni dan jumlahDiminta. Modifikasi class Rekening dari Materi 17 supaya method tarik() melempar exception ini alih-alih RuntimeException generik. Tangkap dan tampilkan pesan yang memakai kedua properti itu.
Materi 24 · Modul 4
Konfigurasi dan environment
Memisahkan pengaturan (kredensial, kunci API) dari kode — supaya aman dan mudah berpindah lingkungan.
Masalah menaruh config langsung di kode
<?php
// password database langsung di kode — berbahaya
$pdo = new \PDO("mysql:host=localhost;dbname=toko", "root", "password123");Masalahnya: kalau file ini ter-commit ke Git (bahkan ke repository publik), passwordnya bocor. Dan setiap kamu pindah dari komputer lokal ke server produksi, kamu harus mengubah kode-nya langsung — rawan kesalahan.
File .env: pengaturan di luar kode
DB_HOST=localhost
DB_NAME=toko
DB_USER=root
DB_PASS=password123
APP_ENV=local
APP_DEBUG=truecomposer require vlucas/phpdotenv<?php
require "vendor/autoload.php";
$dotenv = \Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// sekarang isi .env bisa diakses lewat $_ENV atau getenv()
$pdo = new \PDO(
"mysql:host=" . $_ENV["DB_HOST"] . ";dbname=" . $_ENV["DB_NAME"],
$_ENV["DB_USER"],
$_ENV["DB_PASS"],
);Hati-hati — File .env berisi rahasia, jadi wajib ditambahkan ke .gitignore supaya tidak ikut ter-commit. Sertakan file .env.example (tanpa nilai sungguhan, cuma nama variabelnya) di repository sebagai contoh untuk anggota tim atau server baru.
Class Config sederhana
Daripada mengakses $_ENV[...] tersebar di mana-mana, bungkus dalam satu class supaya rapi dan gampang diberi nilai default.
<?php
declare(strict_types=1);
namespace App;
class Config {
public static function get(string $key, mixed $default = null): mixed {
return $_ENV[$key] ?? $default;
}
public static function isProduksi(): bool {
return self::get("APP_ENV") === "production";
}
}
// pakai di mana saja
$host = Config::get("DB_HOST", "localhost");
if (Config::isProduksi()) {
ini_set("display_errors", "0"); // matikan tampilan error di produksi
}Latihan — Buat file .env dengan pengaturan database dan APP_ENV. Pasang vlucas/phpdotenv, buat class Config seperti di atas, dan pakai untuk mengatur error_reporting secara berbeda antara local dan production.
Materi 25 · Modul 4
Struktur project dan routing sederhana
Menyusun banyak halaman lewat satu pintu masuk — pondasi cara kerja hampir semua framework PHP modern.
Masalah banyak file .php terpisah
Sejauh ini tiap "halaman" adalah file .php sendiri (produk.php, login.php, dst). Ini bekerja, tapi merepotkan untuk hal-hal yang seharusnya berlaku ke semua halaman (autentikasi, header keamanan, penanganan error seragam). Solusinya: satu pintu masuk yang mengatur semua permintaan.
Front controller: satu pintu masuk
<?php
declare(strict_types=1);
require __DIR__ . "/../vendor/autoload.php";
// ambil bagian path dari URL, misal "/produk/5"
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$method = $_SERVER["REQUEST_METHOD"];
// daftar rute: [method, pola, apa yang dijalankan]
$routes = [
["GET", "/", [HomeController::class, "index"]],
["GET", "/produk", [ProdukController::class, "daftar"]],
["POST", "/produk", [ProdukController::class, "simpan"]],
];
foreach ($routes as [$m, $pola, $aksi]) {
if ($m === $method && $pola === $path) {
[$class, $fn] = $aksi;
$controller = new $class();
$controller->$fn();
exit;
}
}
http_response_code(404);
echo "404 — Halaman tidak ditemukan";Catatan — Ini versi paling sederhana dari routing — konsep inti di balik semua framework PHP (Laravel, Symfony, Slim). Satu file menerima semua permintaan, lalu memutuskan kode mana yang harus jalan berdasarkan URL dan method-nya. Framework sungguhan menambah banyak fitur (parameter dinamis /produk/{id}, middleware, grup rute), tapi prinsip dasarnya sama seperti di atas.
Controller: mengelompokkan aksi terkait
<?php
declare(strict_types=1);
namespace App\Controllers;
class ProdukController {
public function daftar(): void {
$produk = [/* ambil dari database nanti */];
require __DIR__ . "/../Views/produk-daftar.php";
}
public function simpan(): void {
// validasi $_POST, simpan, redirect — seperti Materi 12
header("Location: /produk");
exit;
}
}Kenapa server perlu diarahkan ke public/index.php
Semua permintaan (apa pun URL-nya) harus diarahkan ke satu file index.php. Ini diatur lewat konfigurasi web server.
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]Aturan ini berarti: "kalau URL yang diminta bukan file sungguhan (bukan gambar, CSS, dll), arahkan semuanya ke index.php." Dari situ, kode routing kita yang mengurus sisanya.
Struktur project lengkap sejauh ini
toko-app/
├── .env
├── .env.example
├── .gitignore
├── composer.json
├── vendor/
├── public/
│ ├── index.php ← satu-satunya pintu masuk
│ └── .htaccess
└── src/
├── Config.php
├── Controllers/
│ └── ProdukController.php
├── Models/
│ └── Produk.php
├── Views/
│ └── produk-daftar.php
└── Exceptions/
└── StokTidakCukupException.phpCatatan — Perhatikan: public/ adalah satu-satunya folder yang diarahkan web server ke internet. Folder src/, vendor/, dan .env tidak bisa diakses langsung lewat URL — jadi kode dan rahasia lebih aman. Struktur "satu pintu masuk + folder public terpisah" ini persis yang dipakai framework modern seperti Laravel dan Symfony.
Latihan — Susun ulang project latihan-latihan sebelumnya (form kontak, sistem login sederhana) ke struktur ini: satu public/index.php dengan tabel routing, controller terpisah untuk tiap fitur, dan view di folder Views/. Uji semua rute masih berfungsi.
Materi 26 · Modul 5
JSON dan membangun API sederhana
Membuat PHP berbicara dengan aplikasi lain (mobile app, JavaScript frontend) lewat format data universal.
JSON: format tukar-data universal
<?php
$produk = ["nama" => "Kaos", "harga" => 85000, "tags" => ["promo", "baru"]];
// array PHP → teks JSON
$json = json_encode($produk);
// {"nama":"Kaos","harga":85000,"tags":["promo","baru"]}
// teks JSON → array PHP
$data = json_decode($json, true); // true = hasilkan array, bukan object
echo $data["nama"]; // Kaos
// JSON rapi untuk dibaca manusia (debugging)
echo json_encode($produk, JSON_PRETTY_PRINT);Endpoint API sederhana
Sebuah "API" pada dasarnya adalah halaman PHP yang membalas dengan JSON alih-alih HTML. Aplikasi lain (React, aplikasi mobile) memanggilnya dan mengurai balasannya sendiri.
<?php
declare(strict_types=1);
// beri tahu klien bahwa balasannya JSON
header("Content-Type: application/json");
$produk = [
["id" => 1, "nama" => "Kaos", "harga" => 85000],
["id" => 2, "nama" => "Hoodie", "harga" => 210000],
];
echo json_encode(["data" => $produk]);Membaca body JSON yang dikirim klien (misalnya lewat POST dari JavaScript fetch) sedikit berbeda dari form biasa — datanya tidak ada di $_POST, tapi di raw input:
<?php
header("Content-Type: application/json");
// baca body mentah, lalu urai sebagai JSON
$body = file_get_contents("php://input");
$data = json_decode($body, true);
if ($data === null) {
http_response_code(400);
echo json_encode(["message" => "JSON tidak valid"]);
exit;
}
$nama = trim($data["nama"] ?? "");
if ($nama === "") {
http_response_code(422);
echo json_encode(["message" => "Validasi gagal", "errors" => ["nama" => "Nama wajib diisi"]]);
exit;
}
http_response_code(201);
echo json_encode(["message" => "Tersimpan", "data" => $data]);Catatan — Kode status HTTP penting di API: 200 untuk sukses membaca, 201 untuk sukses membuat data baru, 400 untuk request yang rusak, 422 untuk validasi gagal, 404 untuk data tak ditemukan. Klien (frontend) biasanya memutuskan tindakannya berdasarkan kode ini, bukan cuma isi pesannya.
Latihan — Buat api/produk.php yang membalas daftar produk sebagai JSON. Buat api/produk-simpan.php yang menerima JSON lewat POST, memvalidasi nama dan harga, dan membalas dengan status serta pesan yang sesuai (201 kalau berhasil, 422 dengan detail error kalau gagal).
Materi 27 · Modul 5
Pratinjau: PHP bicara dengan database
Sekilas cara PHP terhubung ke MySQL, sebagai jembatan ke handbook database.
PDO: cara standar PHP mengakses database
PDO (PHP Data Objects) adalah lapisan bawaan PHP untuk berbicara dengan berbagai jenis database (MySQL, PostgreSQL, SQLite) lewat satu cara pemakaian yang sama. Ini pratinjau singkat — pembahasan penuh soal query, JOIN, transaksi, dan desain tabel ada di handbook MySQL terpisah.
<?php
declare(strict_types=1);
// membuat koneksi
$pdo = new \PDO(
"mysql:host=localhost;dbname=toko;charset=utf8mb4",
"root",
"",
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
);
// mengambil data — perhatikan tanda ? sebagai placeholder, BUKAN menyambung string
$stmt = $pdo->prepare("SELECT * FROM produk WHERE stok > ?");
$stmt->execute([0]);
$produk = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// $produk sekarang array asosiatif — persis seperti Materi 6
foreach ($produk as $p) {
echo $p["nama"];
}Hati-hati — Perhatikan tanda ? di query, lalu nilainya dikirim terpisah lewat execute([...]). Ini yang disebut prepared statement, dan ini yang mencegah SQL injection (Materi 15). Nilai dari pengguna tidak pernah disambung langsung ke teks query. Handbook MySQL membahas ini secara mendalam.
Kenapa dipelajari terpisah
PHP (bahasa dan logikanya) dan MySQL (cara menyusun tabel, menulis query, index, transaksi) adalah dua keterampilan berbeda yang kebetulan sering dipakai bersama. Memisahkan pembelajarannya membuat masing-masing lebih dalam dan tidak tercampur aduk. Urutan yang disarankan:
- Selesaikan handbook PHP ini sampai akhir.
- Pelajari handbook MySQL dari nol — tanpa PHP sama sekali, murni SQL.
- Pelajari handbook studi kasus, yang menyatukan keduanya jadi aplikasi CRUD utuh dengan PDO.
Latihan — Tidak ada latihan praktik untuk materi ini — cukup pahami konsepnya. Nanti di handbook studi kasus, kamu akan langsung praktik penuh menghubungkan kode seperti Modul 4 ini dengan tabel MySQL sungguhan.
Materi 28 · Modul 5
Testing dasar dengan PHPUnit
Menulis kode yang memeriksa kode lain — supaya perubahan di masa depan tidak diam-diam merusak sesuatu.
Kenapa testing
Sejauh ini kamu menguji kode dengan menjalankannya manual dan melihat hasilnya. Itu bekerja untuk project kecil, tapi merepotkan untuk project besar: setiap perubahan kecil, kamu harus mengecek ulang semua fitur secara manual. Test otomatis menjalankan pengecekan itu untukmu, dalam hitungan detik.
Pasang dan tulis test pertama
composer require --dev phpunit/phpunit<?php
declare(strict_types=1);
namespace Tests;
use PHPUnit\Framework\TestCase;
use function App\rupiah;
class HargaTest extends TestCase {
// nama method HARUS diawali "test"
public function testFormatRupiahDasar(): void {
$this->assertSame("Rp 85.000", rupiah(85000));
}
public function testFormatRupiahNol(): void {
$this->assertSame("Rp 0", rupiah(0));
}
public function testDiskonMengurangiHarga(): void {
$hasil = hitungDiskon(100000, 10.0);
$this->assertSame(90000, $hasil);
}
}./vendor/bin/phpunit tests
// output kalau semua lulus:
// OK (3 tests, 3 assertions)
// output kalau ada yang gagal:
// FAILED: testDiskonMengurangiHarga
// Failed asserting that 89000 matches expected 90000.Catatan — assertSame membandingkan nilai dan tipe (setara dengan ===). Ada banyak assertion lain: assertTrue, assertCount, assertNull, assertInstanceOf. Pola umum sebuah test: siapkan data → jalankan fungsi yang diuji → periksa hasilnya sesuai harapan.
Test untuk exception
<?php
declare(strict_types=1);
namespace Tests;
use App\Models\Rekening;
use PHPUnit\Framework\TestCase;
class RekeningTest extends TestCase {
public function testTarikMelebihiSaldoMelemparError(): void {
$akun = new Rekening();
$akun->setor(50000);
// beri tahu PHPUnit: kita MENGHARAPKAN exception ini dilempar
$this->expectException(\RuntimeException::class);
$akun->tarik(100000); // harus melempar RuntimeException
}
}Kapan (dan tidak) menulis test
Sebagai pemula, jangan tergoda menguji semuanya. Prioritaskan:
- Logika bisnis penting. Perhitungan harga, diskon, validasi — bagian yang kalau salah, langsung merugikan.
- Fungsi murni. Fungsi yang hasilnya cuma bergantung pada inputnya (seperti
rupiah()) paling mudah dan paling berharga untuk diuji. - Bug yang pernah terjadi. Setiap kali menemukan bug, tulis test yang menangkapnya — supaya bug yang sama tidak muncul lagi diam-diam.
Tidak perlu menguji semua halaman HTML atau kode yang sangat sederhana — itu buang waktu untuk manfaat kecil.
Latihan — Tulis test untuk fungsi bagiAman() dari Materi 10: satu test untuk kasus normal, satu test yang mengharapkan InvalidArgumentException saat dibagi nol. Jalankan dengan PHPUnit dan pastikan keduanya lulus.
Materi 29 · Modul 5
Keamanan lanjutan
Melengkapi dasar keamanan dari Materi 15 dengan praktik tingkat produksi.
HTTPS wajib untuk produksi
Tanpa HTTPS, semua data (termasuk password) dikirim polos lewat jaringan dan bisa disadap. Di produksi, pasang sertifikat SSL (banyak hosting menyediakan gratis lewat Let's Encrypt) dan paksa semua trafik memakai HTTPS.
<?php
if (empty($_SERVER["HTTPS"]) && Config::isProduksi()) {
$url = "https://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
header("Location: $url", true, 301);
exit;
}Rate limiting: membatasi percobaan
Tanpa pembatasan, penyerang bisa mencoba ribuan kombinasi password dalam hitungan detik (brute force). Batasi jumlah percobaan dalam rentang waktu tertentu.
<?php
session_start();
$percobaan = $_SESSION["percobaan_login"] ?? 0;
$terakhir = $_SESSION["waktu_terakhir"] ?? 0;
// reset penghitung kalau sudah lewat 15 menit sejak percobaan terakhir
if (time() - $terakhir > 900) {
$percobaan = 0;
}
if ($percobaan >= 5) {
die("Terlalu banyak percobaan. Coba lagi dalam 15 menit.");
}
// ... proses login ...
// kalau login GAGAL:
$_SESSION["percobaan_login"] = $percobaan + 1;
$_SESSION["waktu_terakhir"] = time();
// kalau login BERHASIL: reset
unset($_SESSION["percobaan_login"]);Catatan — Contoh di atas berbasis session, cukup untuk belajar. Di produksi dengan trafik nyata, rate limiting biasanya diletakkan di level yang lebih andal — lewat database (mencatat percobaan per alamat IP atau akun) atau di web server / reverse proxy itu sendiri.
Header keamanan HTTP
<?php
header("X-Content-Type-Options: nosniff"); // cegah browser menebak tipe file
header("X-Frame-Options: DENY"); // cegah situsmu dimuat di iframe orang lain
header("Referrer-Policy: same-origin"); // batasi info referrer yang dikirimChecklist keamanan sebelum go-live
- ✅
display_errorsdimatikan di produksi (Materi 10). - ✅ Semua query database pakai prepared statement, tanpa kecuali (Materi 15).
- ✅ Semua output dari pengguna dibungkus
htmlspecialchars()(Materi 15). - ✅ Password memakai
password_hash/password_verify, tidak pernah disimpan polos (Materi 15). - ✅ Form yang mengubah data punya token CSRF (Materi 15).
- ✅ File
.envtidak ter-commit ke Git, dan tidak bisa diakses lewat URL. - ✅ HTTPS aktif dan dipaksakan.
- ✅ Upload file divalidasi dari isinya, disimpan dengan nama acak (Materi 14).
- ✅ Ada pembatasan percobaan untuk login dan endpoint sensitif lain.
Latihan — Ambil sistem login dari latihan Materi 13. Tambahkan rate limiting: maksimal 5 percobaan gagal dalam 15 menit, lalu tolak percobaan berikutnya dengan pesan yang jelas.
Materi 30 · Modul 5 · Penutup
Deployment dan langkah selanjutnya
Membawa aplikasi dari komputer lokal ke server sungguhan, dan peta jalan setelah handbook ini.
Menyiapkan server produksi
- Pasang PHP versi yang sama dengan yang dipakai untuk mengembangkan (cek dengan
php -v), plus ekstensi yang dibutuhkan (biasanyapdo_mysql,mbstring,openssl). - Upload kode — lewat Git (disarankan, supaya mudah update) atau FTP/SFTP.
- Jalankan
composer installdi server (bukan meng-upload foldervendor/— biarkan Composer yang membuatnya ulang di sana). - Buat file
.envlangsung di server dengan kredensial produksi (jangan pernah upload.envlokal). - Arahkan document root web server ke folder
public/, bukan ke akar project (Materi 25). - Set
APP_ENV=productionsupayadisplay_errorsotomatis mati.
Proses deploy yang lebih rapi
1. git push ke server (atau lewat CI/CD)
2. composer install --no-dev --optimize-autoloader
3. Jalankan migrasi database (dibahas di handbook MySQL/studi kasus)
4. Restart PHP-FPM / web server kalau perlu--no-dev mengecualikan library yang hanya untuk development (seperti PHPUnit) — bikin instalasi produksi lebih ringkas. --optimize-autoloader mempercepat proses autoload PSR-4 yang kita bahas di Materi 22.
Logging di produksi
<?php
// arahkan error ke file log, bukan ditampilkan
ini_set("log_errors", "1");
ini_set("error_log", __DIR__ . "/../storage/logs/error.log");
// catat kejadian penting secara manual juga
error_log("[PESANAN] Order #123 dibuat oleh user #45");Saat ada masalah di produksi, log file inilah tempat pertama yang dicek — jauh lebih berguna daripada menebak-nebak apa yang salah tanpa jejak apa pun.
Yang sudah kamu kuasai
- Modul 0–1 — dasar bahasa: variabel, tipe, kondisi, perulangan, fungsi, array, error handling.
- Modul 2 — PHP untuk web: form, session, cookie, upload, keamanan dasar (XSS, CSRF, password).
- Modul 3 — OOP penuh: class, enkapsulasi, inheritance, interface, trait, enum, static.
- Modul 4 — PHP modern: Composer, namespace, autoload, exception custom, konfigurasi, struktur project.
- Modul 5 — topik produksi: JSON/API, pratinjau database, testing, keamanan lanjutan, deployment.
Langkah selanjutnya
Urutan yang disarankan — Handbook ini sengaja berhenti di batas PHP murni. Langkah berikutnya: handbook MySQL dari nol — mempelajari SQL secara mendalam tanpa PHP sama sekali (SELECT, JOIN, index, normalisasi, transaksi). Setelah itu, handbook studi kasus menyatukan PHP dan MySQL jadi satu aplikasi CRUD utuh dengan PDO, autentikasi sungguhan, dan struktur project produksi — mempraktikkan semua yang sudah dipelajari di kedua handbook sekaligus.
Kalau kamu sudah terbiasa dengan sebuah framework PHP (Laravel, Symfony, atau lainnya) di kemudian hari, semua konsep di handbook ini akan langsung terlihat — routing, controller, dependency lewat Composer, konfigurasi environment, semuanya dibangun dari fondasi yang sama persis dengan yang baru saja kamu pelajari.