Menghubungkan PHP dengan database: koneksi PDO ke MySQL/PostgreSQL/SQLite, prepared statements sebagai tameng SQL injection, transaksi ACID untuk konsistensi data, lalu ORM Eloquent dan Doctrine serta query builder dan relationships untuk produktivitas pengembangan.

Sampai sini, semua data kita hidup dalam memori dan hilang saat request selesai. Episode 11 mengubah itu: kalian akan menyimpan, membaca, dan mengubah data secara persisten lewat database. Ini adalah momen "aplikasi kalian menjadi nyata" — dari skrip latihan menjadi sistem yang melayani pengguna sungguhan.
Mengapa database penting? Hampir semua aplikasi bisnis — toko online, CMS, dashboard — adalah "antarmuka di atas data". Cara kalian mengakses data menentukan keamanan (SQL injection), konsistensi (transaksi), dan performa (query). Kita mulai dengan PDO yang aman, lalu ke Eloquent sebagai ORM yang kalian temui di episode 13+.
PDO (PHP Data Objects) adalah satu API untuk banyak database. Koneksi:
<?php
declare(strict_types=1);
$dsn = "mysql:host=127.0.0.1;port=3306;dbname=belajar_php;charset=utf8mb4";
$user = "app";
$pass = getenv("DB_PASS") ?: "local-secret";
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);Tiga atribut di atas adalah standar produksi:
ERRMODE_EXCEPTION — error jadi exception yang bisa ditangkap (episode 8).FETCH_ASSOC — hasil query berupa array asosiatif.EMULATE_PREPARES => false — pakai prepared statement sungguhan di sisi database (kunci keamanan).Ganti DSN untuk database lain:
| Database | DSN |
|---|---|
| MySQL | mysql:host=127.0.0.1;dbname=app;charset=utf8mb4 |
| PostgreSQL | pgsql:host=127.0.0.1;dbname=app |
| SQLite | sqlite:/path/ke/file.db |
Jangan pernah menaruh kredensial di kode — baca dari environment variable (detail di episode 19).
Inilah aturan nomor satu keamanan database. JANGAN pernah menyusun query dengan concatenation:
// ⛔ JANGAN PERNAH
$id = $_GET["id"];
$pdo->query("SELECT * FROM users WHERE id = $id");Jika $id berisi 1 OR 1=1, seluruh tabel bocor. Yang benar — prepared statement:
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ? AND aktif = ?");
$stmt->execute([$id, 1]);
$user = $stmt->fetch();
// atau named placeholder untuk kejelasan
$stmt = $pdo->prepare(
"SELECT * FROM users WHERE email = :email AND aktif = :aktif"
);
$stmt->execute([":email" => $email, ":aktif" => true]);Database menerima nilai dan query secara terpisah — input pengguna tidak pernah bisa menjadi bagian dari struktur SQL. Ini tameng utama terhadap kelas serangan yang kita dalami di episode 17.
// INSERT dengan last insert id
$stmt = $pdo->prepare(
"INSERT INTO users (nama, email) VALUES (:nama, :email)"
);
$stmt->execute([":nama" => $nama, ":email" => $email]);
$id = (int) $pdo->lastInsertId();
// SELECT semua baris
$rows = $pdo
->query("SELECT * FROM users ORDER BY id DESC")
->fetchAll();
// UPDATE
$pdo->prepare("UPDATE users SET nama = :nama WHERE id = :id")
->execute([":nama" => $namaBaru, ":id" => $id]);
// DELETE
$pdo->prepare("DELETE FROM users WHERE id = :id")
->execute([":id" => $id]);fetch() mengambil satu baris, fetchAll() semua baris. Untuk jumlah besar, pertimbangkan streaming dengan yield (episode 20).
Transaksi menjamin sekelompok operasi semua-atau-tidak-sama:
$pdo->beginTransaction();
try {
$pdo->prepare("UPDATE akun SET saldo = saldo - ? WHERE id = ?")
->execute([100000, $dari]);
$pdo->prepare("UPDATE akun SET saldo = saldo + ? WHERE id = ?")
->execute([100000, $ke]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}Tanpa transaksi, jika UPDATE kedua gagal, uang hilang dari akun $dari tapi tidak masuk ke akun $ke. Transaksi (ACID) mencegah state korup seperti ini. Ingat pola tetap: begin → operasi → commit, dan rollBack di catch.
Tip
Rule of thumb: setiap operasi yang mengubah lebih dari satu baris atau baris yang saling bergantung (transfer, order + stok, payment + status) layak dibungkus transaksi. Operasi tunggal yang sederhana tidak perlu.
Menulis SQL manual di setiap tempat cepat lelah dan rawan error. ORM memetakan tabel menjadi class dan baris menjadi object:
Contoh model Eloquent:
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $fillable = ["nama", "email"];
public function orders()
{
return $this->hasMany(Order::class);
}
}Query builder / Eloquent memakai data yang sama dan tetap aman dari injection karena memakai binding di belakang layar:
$users = User::query()
->where("aktif", true)
->where("usia", ">=", 18)
->orderBy("nama")
->limit(10)
->get();
$totalBelanja = User::query()
->findOrFail($id)
->orders()
->sum("total");Kapan memilih mana:
| Pendekatan | Kelebihan | Dipakai kapan |
|---|---|---|
| PDO langsung | Transparan, kontrol penuh, tanpa overhead | Skrip sederhana, performance-critical, non-framework |
| Eloquent | Produktif, hubungan antar-model intuitif | Aplikasi Laravel (default) |
| Doctrine | Isolasi domain kuat, mapping eksplisit | Aplikasi Symfony kompleks |
Warning
ORM bukan pengganti pemahaman SQL — malah sebaliknya. Kapan kalian tidak paham SQL, kalian tidak akan tahu kenapa Eloquent menghasilkan query aneh atau N+1 (episode 18). Kuasai SQL dan PDO dulu, ORM menjadi alat yang mempercepat, bukan kotak hitam.
utf8 alih-alih utf8mb4 di MySQL — emoji dan karakter 4-byte rusak; selalu utf8mb4.password_hash (episode 14).SELECT * terus-menerus boros; pilih kolom yang dibutuhkan.Inti yang harus dibawa pulang:
ERRMODE_EXCEPTION + FETCH_ASSOC + EMULATE_PREPARES=false.Di episode 12 selanjutnya kita menjembatani PHP dengan pengguna: Form Handling & HTTP Basics — $_GET/$_POST, validasi input, upload file, lalu headers, redirect, JSON response, cookies, dan session dengan secure flags. Di sini PHP menunjukkan keunggulan utamanya sebagai bahasa web.