Sebagian besar situs PHP bertemu PostgreSQL bukan lewat psql, melainkan lewat kode aplikasi: halaman katalog yang membaca tabel produk, formulir pendaftaran yang menulis baris pelanggan, laporan harian Toko Maju Jaya yang menggabungkan banyak tabel menjadi satu tampilan. Chapter pertama section ini menyambungkan PHP ke PostgreSQL lewat PDO — lapisan akses database bawaan PHP yang API-nya seragam untuk berbagai server database. Kita akan menempuh jalur standar produksi: koneksi pertama, query berparameter, transaksi, lalu penanganan error dengan kode SQLSTATE. Seluruh pola yang dibahas di sini juga menjadi fondasi yang dipakai framework PHP modern di balik layar.

PDO: Standar Akses Database di PHP #

PHP memiliki dua cara historis untuk berbicara dengan PostgreSQL: fungsi lama pg_connect()/pg_query() dari ekstensi pgsql, dan PDO (PHP Data Objects). PDO adalah pilihan yang layak dipertahankan untuk kode baru karena tiga alasan. Pertama, API PDO tidak berubah bila Anda berpindah server database — nama fungsi tetap prepare(), execute(), fetchAll(); hanya DSN dan sedikit dialek SQL yang menyesuaikan. Kedua, PDO memaksa disiplin prepared statement: query dan data dikirim terpisah sejak API-nya dirancang. Ketiga, ekstensi pdo_pgsql diurus aktif oleh tim PHP dan mengikuti rilis PostgreSQL terbaru.

Dua ekstensi terlibat saat PHP berbicara dengan PostgreSQL: pdo (API umum, sudah aktif di PHP modern) dan pdo_pgsql (driver spesifik PostgreSQL, dibangun di atas pustaka klien libpq). Ekstensi kedua inilah yang harus dipasang.

Instalasi Ekstensi pdo_pgsql #

Di Debian dan Ubuntu, satu paket menghadirkan driver PDO untuk PostgreSQL:

  1. Pasang paket: sudo apt install php-pgsql.
  2. Pastikan ekstensi aktif di php.ini: baris extension=pdo_pgsql (paket Debian biasanya mengaktifkannya lewat berkas konfigurasi tersendiri di conf.d).
  3. Periksa hasilnya: php -m | grep pdo_pgsql — nama pdo_pgsql tampil berarti driver siap.

Koneksi Pertama: DSN dan Opsi PDO #

PDO dibangun di atas konsep DSN (Data Source Name) — satu string yang menyatakan driver dan parameter koneksi. Untuk PostgreSQL, prefiks DSN adalah pgsql: dan parameternya dipisah titik koma, mengikuti gaya libpq:

php
<?php
$dsn      = 'pgsql:host=localhost;port=5432;dbname=toko_db';
$user     = 'postgres';
$password = 'YOUR_PASSWORD';

$opsi = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];

try {
    $pdo = new PDO($dsn, $user, $password, $opsi);
} catch (PDOException $e) {
    error_log('Koneksi gagal: ' . $e->getMessage());
    exit('Maaf, layanan sedang tidak tersedia.');
}

Mari kita bedah baris-baris pentingnya:

  1. Baris DSN ($dsn = 'pgsql:...') — host dan port menunjuk alamat server PostgreSQL; dbname memilih database. Bila parameter kosong, driver mengisi dari variabel environment libpq (PGHOST, PGPORT, PGDATABASE) — mekanisme yang sama dengan psql.
  2. Opsi PDO::ATTR_ERRMODE — nilai PDO::ERRMODE_EXCEPTION membuat setiap galat SQL melempar PDOException, bukan diam atau mengembalikan false. Tanpa opsi ini, kode cenderung lupa memeriksa hasil kembalian.
  3. Opsi PDO::ATTR_DEFAULT_FETCH_MODE — menetapkan bentuk baris hasil untuk seluruh fetch() di koneksi ini; PDO::FETCH_ASSOC menghasilkan array asosiatif ($row['nama']).
  4. Blok try/catch — koneksi gagal (server mati, kata sandi salah, database tidak ada) melempar PDOException; pesannya dicatat ke log, sedangkan pengguna mendapat pesan generik tanpa detail internal.

Bahan Latihan: Tabel Produk #

Contoh-contoh berikutnya bekerja di atas tabel produk Toko Maju Jaya. File berikut menjalankan DDL dan data awal lewat psql, persis seperti chapter-chapter SQL sebelumnya:

-- Bahan latihan chapter PHP: tabel produk Toko Maju Jaya.
-- Tabel ini dibuat dengan psql; seluruh contoh kode PHP pada
-- chapter ini bekerja di atas tabel yang sama.
DROP TABLE IF EXISTS produk;

CREATE TABLE produk (
    id    integer PRIMARY KEY,
    nama  text NOT NULL,
    harga bigint NOT NULL CHECK (harga >= 0),
    stok  integer NOT NULL DEFAULT 0
);

INSERT INTO produk (id, nama, harga, stok) VALUES
    (1, 'Mouse Kantor Ergonomis', 85000, 25),
    (2, 'Papan Ketik Mekanik', 750000, 12),
    (3, 'Monitor 24 inci IPS', 1850000, 8),
    (4, 'Kabel HDMI 2 meter', 65000, 40);

Output yang Dihasilkan:

text
DROP TABLE
CREATE TABLE
INSERT 0 4

Dengan tabel itu di tempatnya, kode PHP tinggal membacanya — dan seluruh akses data selanjutnya memakai prepared statement.

Query dengan Parameter #

Chapter SQL Injection membedah apa yang terjadi bila input pengguna ditempelkan mentah ke string SQL. Di PHP, pertahanannya adalah prepare() diikuti execute(): teks SQL dikirim dengan lubang parameter, nilai dikirim terpisah, dan server tidak pernah mem-parse nilai sebagai SQL. PDO mendukung dua gaya penanda.

Penanda Posisi ? #

Penanda ? diisi berurutan sesuai urutan elemen array yang diberikan ke execute():

php
<?php
$stmt = $pdo->prepare(
    'SELECT id, nama, harga FROM produk WHERE harga <= ? ORDER BY id'
);
$stmt->execute([200000]);

foreach ($stmt->fetchAll() as $produk) {
    echo $produk['id'], '. ', $produk['nama'],
         ' Rp', number_format($produk['harga'], 0, ',', '.'), PHP_EOL;
}

Query di atas mengembalikan Mouse Kantor Ergonomis dan Kabel HDMI 2 meter — dua produk dengan harga di bawah Rp200.000. Fungsi number_format() dengan argumen ',' dan '.' memformat harga bergaya Indonesia (85000 menjadi 85.000).

Penanda Bernama :nama #

Untuk query dengan banyak parameter, penanda bernama lebih tahan salah urutan:

php
<?php
$stmt = $pdo->prepare(
    'INSERT INTO produk (id, nama, harga, stok)
     VALUES (:id, :nama, :harga, :stok)
     RETURNING id, nama'
);
$stmt->execute([
    'id'    => 5,
    'nama'  => 'Lampu Meja LED',
    'harga' => 120000,
    'stok'  => 20,
]);
$baru = $stmt->fetch();
echo "Tersimpan: {$baru['id']} - {$baru['nama']}", PHP_EOL;

Klausa RETURNING membuat INSERT sekaligus mengembalikan barisnya, sehingga tidak perlu query SELECT kedua untuk membaca hasilnya.

Mode Fetch dan Bentuk Baris #

Satu hasil query bisa dibaca dalam beberapa bentuk. Mode default sudah diatur lewat PDO::ATTR_DEFAULT_FETCH_MODE saat koneksi dibuat, tetapi mode bisa juga dipilih per panggilan lewat argumen fetchAll():

ModeBentuk setiap barisAkses kolom
PDO::FETCH_ASSOCarray asosiatif ['nama' => ..., 'harga' => ...]$row['nama']
PDO::FETCH_NUMarray berindeks angka [0 => ..., 1 => ...]$row[1]
PDO::FETCH_OBJobjek stdClass$row->nama
php
<?php
$stmt = $pdo->prepare(
    'SELECT nama, harga FROM produk WHERE stok < ? ORDER BY id'
);
$stmt->execute([20]);

// Bentuk array: kolom dipanggil lewat indeks.
foreach ($stmt->fetchAll(PDO::FETCH_NUM) as [$nama, $harga]) {
    echo $nama, ' (stok menipis) ', number_format($harga, 0, ',', '.'), PHP_EOL;
}

Mode FETCH_ASSOC cocok untuk kode yang membaca banyak kolom bernama; FETCH_NUM hemat dan cocok saat urutan kolom jelas (seperti destrukturisasi [$nama, $harga] di atas); FETCH_OBJ nyaman di template yang menulis $produk->nama.

Transaksi: beginTransaction, commit, rollBack #

Transaksi menjamin beberapa perubahan terlihat sebagai satu kesatuan — materi lengkapnya ada di chapter transaksi section sebelumnya; di PDO, siklusnya dipanggil lewat tiga metode. Pola berikut memindahkan stok antarproduk dan membatalkan semuanya bila salah satu langkah gagal:

php
<?php
$pdo->beginTransaction();
try {
    $kurang = $pdo->prepare(
        'UPDATE produk SET stok = stok - :jumlah WHERE id = :id'
    );
    $tambah = $pdo->prepare(
        'UPDATE produk SET stok = stok + :jumlah WHERE id = :id'
    );

    $kurang->execute(['jumlah' => 2, 'id' => 2]);
    $tambah->execute(['jumlah' => 2, 'id' => 1]);

    $pdo->commit();
    echo 'Stok dipindahkan.', PHP_EOL;
} catch (PDOException $e) {
    $pdo->rollBack();
    error_log('Transaksi dibatalkan: ' . $e->getMessage());
    echo 'Pemindahan stok dibatalkan.', PHP_EOL;
}

Urutannya penting: beginTransaction() menandai awal blok transaksi, commit() menjadikan kedua UPDATE permanen bersama-sama, dan rollBack() di cabang catch membuang keduanya sekaligus. Prepared statement boleh dibuat sebelum atau sesudah beginTransaction() — yang menentukan transaksi adalah kapan execute() berjalan, bukan kapan statement disiapkan.

Menangani PDOException dan SQLSTATE #

Dengan PDO::ERRMODE_EXCEPTION, setiap galat SQL — pelanggaran constraint, syntax error, koneksi terputus — tiba sebagai PDOException. Objek tersebut membawa properti errorInfo: array yang elemen pertamanya adalah kode SQLSTATE lima karakter, standar yang sama di seluruh driver dan didokumentasikan PostgreSQL. Kode itu memungkinkan penanganan yang presisi, bukan sekadar " gagal":

php
<?php
try {
    $stmt = $pdo->prepare(
        'INSERT INTO produk (id, nama, harga, stok) VALUES (?, ?, ?, ?)'
    );
    $stmt->execute([1, 'Produk Duplikat', 100000, 1]);
} catch (PDOException $e) {
    if ($e->errorInfo[0] === '23505') {
        echo 'ID sudah dipakai, gunakan ID lain.', PHP_EOL;
    } else {
        throw $e; // galat yang tak dikenal diteruskan
    }
}

SQLSTATE 23505 berarti unique_violation — sama persis dengan kode yang ditangkap psycopg, pgx, dan node-postgres di chapter-chapter berikutnya; daftar lengkap kodenya ada di Appendix A dokumentasi PostgreSQL. Pola if ... else throw pada contoh di atas penting: hanya galat yang benar-benar Anda pahami yang ditangani; galat tak terduga tetap berfungsi sebagai sinyal bug, bukan ditelan diam-diam.

Satu topik terakhir yang menyambung PHP dengan PostgreSQL di dunia nyata: setiap request halaman yang membuka koneksi baru membayar biaya pembentukan proses backend di server. Opsi PDO::ATTR_PERSISTENT => true membuat PHP memelihara koneksi lintas request, dan di sisi lain ada pooler seperti PgBouncer — keduanya dibahas tuntas di chapter Database Connection Pooling . Sebelum sampai ke sana, mari kita lihat bahasa kedua: Go dengan driver pgx.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

siapkan-tabel.sql

Unduh berkas
sql
-- Bahan latihan chapter PHP: tabel produk Toko Maju Jaya.
-- Tabel ini dibuat dengan psql; seluruh contoh kode PHP pada
-- chapter ini bekerja di atas tabel yang sama.
DROP TABLE IF EXISTS produk;

CREATE TABLE produk (
    id    integer PRIMARY KEY,
    nama  text NOT NULL,
    harga bigint NOT NULL CHECK (harga >= 0),
    stok  integer NOT NULL DEFAULT 0
);

INSERT INTO produk (id, nama, harga, stok) VALUES
    (1, 'Mouse Kantor Ergonomis', 85000, 25),
    (2, 'Papan Ketik Mekanik', 750000, 12),
    (3, 'Monitor 24 inci IPS', 1850000, 8),
    (4, 'Kabel HDMI 2 meter', 65000, 40);