Sepanjang section ini kita menulis PDO secara langsung: new PDO, prepare(), dan execute() bertebaran di setiap skrip. Pola tersebut tidak masalah untuk contoh kecil, tetapi bayangkan aplikasi dengan ratusan query — string SQL yang sama ditulis ulang di banyak berkas, setiap perubahan skema memaksa Anda memburu kode ke mana-mana, dan menguji satu fungsi menjadi mustahil tanpa database yang hidup. Repository pattern menjawab masalah itu dengan satu class yang memegang seluruh akses data untuk satu entitas, sehingga kode aplikasi cukup memanggil method seperti findById() alih-alih menyusun SQL sendiri. Pada chapter penutup section ini kita akan membangun repository PDO untuk data user, mengekstrak interface-nya, lalu membuktikan bahwa implementasi bisa ditukar tanpa menyentuh kode pemanggil. Kajian teori pattern-nya tersedia di chapter Repository Pattern pada Section Design Patterns
; fokus kita di sini adalah sisi integrasi PDO-nya.
Masalah SQL yang Tersebar #
Tanpa repository, query untuk satu entitas yang sama menyebar ke seluruh kode. Query “cari user berdasarkan email” misalnya, dibutuhkan di halaman login, halaman profil, dan panel admin — tiga tempat, tiga salinan string SQL yang harus tetap sinkron. Ketika kolom berubah nama, Anda memburu setiap salinan; ketika satu salinan lupa memakai prepared statement, celah SQL injection terbuka kembali padahal dua salinan lain sudah aman.
Pengujian ikut menderita. Fungsi yang mengeksekusi query langsung hanya bisa diuji dengan database sungguhan, sehingga test suite menjadi lambat dan bergantung pada lingkungan. Repository pattern memusatkan seluruh SQL entitas ke satu class, lalu kode aplikasi berinteraksi hanya lewat method — SQL menjadi detail implementasi yang tersembunyi dan mudah diganti.
Repository Pertama dengan PDO #
Repository paling sederhana adalah class biasa yang menerima objek PDO lewat constructor dan menyediakan satu method per kebutuhan data. Mari kita bangun SqliteUserRepository lengkap dengan lima operasi klasik — findAll, findById, insert, updateEmail, dan delete:
<?php
class SqliteUserRepository
{
public function __construct(private PDO $pdo)
{
}
public function findAll(): array
{
return $this->pdo
->query('SELECT id, nama, email FROM users ORDER BY id')
->fetchAll(PDO::FETCH_ASSOC);
}
public function findById(int $id): ?array
{
$stmt = $this->pdo->prepare('SELECT id, nama, email FROM users WHERE id = ?');
$stmt->execute([$id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
return $user === false ? null : $user;
}
public function insert(string $nama, string $email): int
{
$stmt = $this->pdo->prepare('INSERT INTO users (nama, email) VALUES (?, ?)');
$stmt->execute([$nama, $email]);
return (int) $this->pdo->lastInsertId();
}
public function updateEmail(int $id, string $email): bool
{
$stmt = $this->pdo->prepare('UPDATE users SET email = ? WHERE id = ?');
$stmt->execute([$email, $id]);
return $stmt->rowCount() > 0;
}
public function delete(int $id): bool
{
$stmt = $this->pdo->prepare('DELETE FROM users WHERE id = ?');
$stmt->execute([$id]);
return $stmt->rowCount() > 0;
}
}
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec('CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)');
$repo = new SqliteUserRepository($pdo);
$budiId = $repo->insert('Budi', '[email protected]');
$repo->insert('Siti', '[email protected]');
$user = $repo->findById($budiId);
echo "cari id $budiId: {$user['nama']} <{$user['email']}>\n";
echo $repo->updateEmail($budiId, '[email protected]')
? "email Budi diperbarui\n" : "id tidak ditemukan\n";
echo $repo->delete(999) ? "terhapus\n" : "id 999 tidak ada, delete diabaikan\n";
foreach ($repo->findAll() as $user) {
echo "#{$user['id']} {$user['nama']} <{$user['email']}>\n";
}Output yang Dihasilkan:
cari id 1: Budi <[email protected]>
email Budi diperbarui
id 999 tidak ada, delete diabaikan
#1 Budi <[email protected]>
#2 Siti <[email protected]>Mari kita bedah keputusan-keputusan penting pada class di atas:
__construct(private PDO $pdo)memakai constructor property promotion: PDO masuk sebagai dependency dari luar, bukan diciptakan di dalam class. Class repository tidak perlu tahu DSN, berkas, atau kredensial — class hanya menerima koneksi siap pakai.findAll()adalah satu-satunya method yang boleh memakaiquery()langsung karena tidak memuat input dari luar; seluruh method lain memakai prepared statement sesuai pertahanan chapter SQL Injection Prevention .findById()menerjemahkanfalsedarifetch()menjadinulldengan return type?array. Pemanggil mendapat kontrak yang jelas: user tidak ketika berartinull, bukanfalseyang bertipe campuran.insert()mengembalikan ID baru lewatlastInsertId(), sehingga pemanggil tidak perlu query ulang untuk mengetahui ID-nya.updateEmail()dandelete()memakairowCount() > 0sebagai sinyal:trueberarti barisnya ada dan terpengaruh,falseberarti ID tersebut tidak ditemukan.- Kode pemanggil di bagian bawah tidak lagi memuat satu pun string SQL — hanya nama method yang bermakna bisnis. Skema tabel dipasang di luar class sebagai bagian dari penyiapan lingkungan.
Mengekstrak Interface #
Langkah berikutnya adalah menangkap kontrak repository ke dalam interface: daftar method yang dijanjikan, tanpa implementasi. Nama class SqliteUserRepository jujur mengungkap bahwa class tersebut terikat SQLite; interface memungkinkan kode aplikasi bergantung pada kontrak umum bernama UserRepository saja:
<?php
interface UserRepository
{
public function findAll(): array;
public function findById(int $id): ?array;
public function insert(string $nama, string $email): int;
public function updateEmail(int $id, string $email): bool;
public function delete(int $id): bool;
}
function gantiEmailUser(UserRepository $repo, int $id, string $emailBaru): string
{
if ($repo->findById($id) === null) {
return "user #$id tidak ditemukan";
}
$repo->updateEmail($id, $emailBaru);
$user = $repo->findById($id);
return "email {$user['nama']} kini {$user['email']}";
}Class SqliteUserRepository dari contoh sebelumnya kini cukup dideklarasikan sebagai class SqliteUserRepository implements UserRepository — seluruh method-nya sudah memenuhi tanda tangan interface. Fungsi gantiEmailUser() memperlihatkan manfaatnya: type declaration parameternya adalah interface, bukan class konkret, sehingga fungsi ini menerima semua implementasi UserRepository yang ada maupun yang akan ditulis kelak. Prinsip “bergantung pada abstraksi, bukan implementasi” inilah inti pola ini, dan kedudukannya dalam arsitektur berlapis dibedah tuntas di chapter Controller-Service-Repository
.
Implementasi Kedua untuk Pengujian #
Nilai sesungguhnya dari interface baru terasa ketika implementasi kedua muncul. InMemoryUserRepository menyimpan data pada array PHP biasa — tanpa SQL, tanpa PDO, tanpa database:
<?php
interface UserRepository
{
public function findAll(): array;
public function findById(int $id): ?array;
public function insert(string $nama, string $email): int;
public function updateEmail(int $id, string $email): bool;
public function delete(int $id): bool;
}
class InMemoryUserRepository implements UserRepository
{
/** @var array<int, array{id: int, nama: string, email: string}> */
private array $users = [];
private int $idBerikutnya = 1;
public function findAll(): array
{
return array_values($this->users);
}
public function findById(int $id): ?array
{
return $this->users[$id] ?? null;
}
public function insert(string $nama, string $email): int
{
$id = $this->idBerikutnya++;
$this->users[$id] = ['id' => $id, 'nama' => $nama, 'email' => $email];
return $id;
}
public function updateEmail(int $id, string $email): bool
{
if (!isset($this->users[$id])) {
return false;
}
$this->users[$id]['email'] = $email;
return true;
}
public function delete(int $id): bool
{
if (!isset($this->users[$id])) {
return false;
}
unset($this->users[$id]);
return true;
}
}
function daftarUser(UserRepository $repo, string $label): void
{
echo "== $label ==\n";
foreach ($repo->findAll() as $user) {
echo "#{$user['id']} {$user['nama']} <{$user['email']}>\n";
}
}
$repo = new InMemoryUserRepository();
$repo->insert('Budi', '[email protected]');
$repo->insert('Siti', '[email protected]');
$repo->delete(1);
$repo->updateEmail(2, '[email protected]');
daftarUser($repo, 'InMemoryUserRepository');Output yang Dihasilkan:
== InMemoryUserRepository ==
#2 Siti <[email protected]>Urutan operasi pada contoh sengaja sama dengan skenario di SqliteUserRepository: sisipkan dua user, hapus user pertama, perbarui email user kedua — dan hasilnya pun berbentuk sama. Fungsi daftarUser() sama sekali tidak bisa membedakan implementasi mana yang sedang dipakai; mengganti new InMemoryUserRepository() menjadi new SqliteUserRepository($pdo) hanya menyentuh satu baris. Inilah yang membuat repository pattern begitu berharga bagi pengujian: unit test untuk logika bisnis berjalan pada implementasi in-memory yang secepat array PHP, sementara repository SQL sendiri diuji terpisah dengan SQLite :memory:. Strategi pengujian lengkapnya dibahas di Section Testing dan Quality Assurance
.
Kapan Repository Pattern Layak Dipakai #
Repository pattern membawa struktur berharga sekaligus berkas-berkas tambahan, jadi terapkan dengan sadar. Panduan praktisnya:
| Situasi | Repository layak? |
|---|---|
| Aplikasi dengan logika bisnis dan banyak query | ya — SQL terpusat, mudah dirawat |
| Banyak unit test yang butuh data | ya — implementasi in-memory mempercepat test |
| Skrip CLI sekali jalan atau halaman sederhana | tidak — class tambahan jadi beban |
| Satu repository untuk semua entitas | hindari — pecah per entitas agar tetap fokus |
Dua kesalahan yang paling sering menggerogoti pola ini perlu diantisipasi. Pertama, repository yang membocorkan detail PDO — misalnya mengembalikan PDOStatement ke pemanggil — membatalkan isolasi yang sedang dibangun; kembalikan array, object entitas, atau scalar. Kedua, method yang berkembang menjadi “query builder samar” dengan belasan parameter opsional; ketika satu method butuh lima argumen bertingkat, biarkan SQL-nya menjadi method tersendiri yang jujur namanya. Jika kelak aplikasi membesar hingga butuh menyatukan perubahan banyak repository dalam satu transaksi, pola Unit of Work di Section Design Patterns menjadi pelengkap alaminya.
Ringkasan #
Repository pattern memusatkan seluruh SQL satu entitas ke dalam satu class: PDO masuk lewat constructor, method bernama operasi bisnis, dan kode pemanggil bebas dari string SQL. Interface menjadikan kontrak sebagai ketergantungan, implementasi in-memory membuat pengujian cepat tanpa database, dan pertukaran implementasi hanya menyentuh satu baris. Dengan ini, perjalanan kita melalui PDO — koneksi, prepared statements, binding, fetching, transactions, hingga keamanan — menemukan bentuk organisasi akhirnya. Langkah berikutnya keluar dari kode PHP dan masuk ke perancangan skema itu sendiri: pemodelan relasi dan migrations di Section Database Modeling dan Migrations.